From a22588d2cac0e52861ce4a793da5913d81782c5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 12:51:21 +0000 Subject: [PATCH 01/13] Update MSTest to 3.9.1 (#46458) --- updated-dependencies: - dependency-name: MSTest dependency-version: 3.9.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../snippets/evaluate-safety/EvaluateResponseSafety.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ai/tutorials/snippets/evaluate-safety/EvaluateResponseSafety.csproj b/docs/ai/tutorials/snippets/evaluate-safety/EvaluateResponseSafety.csproj index f1ab54496f41a..5a068a11ab911 100644 --- a/docs/ai/tutorials/snippets/evaluate-safety/EvaluateResponseSafety.csproj +++ b/docs/ai/tutorials/snippets/evaluate-safety/EvaluateResponseSafety.csproj @@ -19,7 +19,7 @@ - + From e2892fbd61baf1e54de95350b0b9ff83970d0ef3 Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Wed, 28 May 2025 06:00:20 -0700 Subject: [PATCH 02/13] revert changes to design guidelines (#46447) --- docs/standard/design-guidelines/arrays.md | 2 +- .../design-guidelines/dependency-properties.md | 2 +- docs/standard/design-guidelines/dispose-pattern.md | 4 ++-- docs/standard/design-guidelines/extension-methods.md | 2 +- .../design-guidelines/guidelines-for-collections.md | 10 +++++----- .../names-of-classes-structs-and-interfaces.md | 4 ++-- docs/standard/design-guidelines/names-of-namespaces.md | 2 +- docs/standard/design-guidelines/property.md | 2 +- docs/standard/design-guidelines/usage-guidelines.md | 8 ++++---- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/standard/design-guidelines/arrays.md b/docs/standard/design-guidelines/arrays.md index a5464434686c2..a4de44b53d95d 100644 --- a/docs/standard/design-guidelines/arrays.md +++ b/docs/standard/design-guidelines/arrays.md @@ -18,7 +18,7 @@ helpviewer_keywords: ✔️ CONSIDER using jagged arrays instead of multidimensional arrays. - A jagged array is an array with elements that are also arrays. The arrays that make up the elements can be of different sizes, leading to less wasted space for some sets of data (for example, sparse matrix) compared to multidimensional arrays. Furthermore, the CLR optimizes index operations on jagged arrays, so they might exhibit better runtime performance in some scenarios. + A jagged array is an array with elements that are also arrays. The arrays that make up the elements can be of different sizes, leading to less wasted space for some sets of data (e.g., sparse matrix) compared to multidimensional arrays. Furthermore, the CLR optimizes index operations on jagged arrays, so they might exhibit better runtime performance in some scenarios. *Portions © 2005, 2009 Microsoft Corporation. All rights reserved.* diff --git a/docs/standard/design-guidelines/dependency-properties.md b/docs/standard/design-guidelines/dependency-properties.md index 65da116bcbc11..64ba0e71297c1 100644 --- a/docs/standard/design-guidelines/dependency-properties.md +++ b/docs/standard/design-guidelines/dependency-properties.md @@ -10,7 +10,7 @@ ms.assetid: 212cfb1e-cec4-4047-94a6-47209b387f6f A dependency property (DP) is a regular property that stores its value in a property store instead of storing it in a type variable (field), for example. - An attached dependency property is a kind of dependency property modeled as static Get and Set methods representing "properties" describing relationships between objects and their containers (for example, the position of a `Button` object on a `Panel` container). + An attached dependency property is a kind of dependency property modeled as static Get and Set methods representing "properties" describing relationships between objects and their containers (e.g., the position of a `Button` object on a `Panel` container). ✔️ DO provide the dependency properties, if you need the properties to support WPF features such as styling, triggers, data binding, animations, dynamic resources, and inheritance. diff --git a/docs/standard/design-guidelines/dispose-pattern.md b/docs/standard/design-guidelines/dispose-pattern.md index 04938bbefac53..e35c03a57956e 100644 --- a/docs/standard/design-guidelines/dispose-pattern.md +++ b/docs/standard/design-guidelines/dispose-pattern.md @@ -26,7 +26,7 @@ All programs acquire one or more system resources, such as memory, system handle Although finalizers are effective in some cleanup scenarios, they have two significant drawbacks: -- The finalizer is called when the GC detects that an object is eligible for collection. This happens at some undetermined period of time after the resource is not needed anymore. The delay between when the developer could or would like to release the resource and the time when the resource is actually released by the finalizer might be unacceptable in programs that acquire many scarce resources (resources that can be easily exhausted) or in cases in which resources are costly to keep in use (for example, large unmanaged memory buffers). +- The finalizer is called when the GC detects that an object is eligible for collection. This happens at some undetermined period of time after the resource is not needed anymore. The delay between when the developer could or would like to release the resource and the time when the resource is actually released by the finalizer might be unacceptable in programs that acquire many scarce resources (resources that can be easily exhausted) or in cases in which resources are costly to keep in use (e.g., large unmanaged memory buffers). - When the CLR needs to call a finalizer, it must postpone collection of the object’s memory until the next round of garbage collection (the finalizers run between collections). This means that the object’s memory (and all objects it refers to) will not be released for a longer period of time. @@ -78,7 +78,7 @@ public class DisposableResourceHolder : IDisposable { } ``` - The Boolean parameter `disposing` indicates whether the method was invoked from the `IDisposable.Dispose` implementation or from the finalizer. The `Dispose(bool)` implementation should check the parameter before accessing other reference objects (for example, the resource field in the preceding sample). Such objects should only be accessed when the method is called from the `IDisposable.Dispose` implementation (when the `disposing` parameter is equal to true). If the method is invoked from the finalizer (`disposing` is false), other objects should not be accessed. The reason is that objects are finalized in an unpredictable order and so they, or any of their dependencies, might already have been finalized. + The Boolean parameter `disposing` indicates whether the method was invoked from the `IDisposable.Dispose` implementation or from the finalizer. The `Dispose(bool)` implementation should check the parameter before accessing other reference objects (e.g., the resource field in the preceding sample). Such objects should only be accessed when the method is called from the `IDisposable.Dispose` implementation (when the `disposing` parameter is equal to true). If the method is invoked from the finalizer (`disposing` is false), other objects should not be accessed. The reason is that objects are finalized in an unpredictable order and so they, or any of their dependencies, might already have been finalized. Also, this section applies to classes with a base that does not already implement the Dispose Pattern. If you are inheriting from a class that already implements the pattern, simply override the `Dispose(bool)` method to provide additional resource cleanup logic. diff --git a/docs/standard/design-guidelines/extension-methods.md b/docs/standard/design-guidelines/extension-methods.md index b77fb5d6b11be..c37995e61e045 100644 --- a/docs/standard/design-guidelines/extension-methods.md +++ b/docs/standard/design-guidelines/extension-methods.md @@ -36,7 +36,7 @@ Extension methods are a language feature that allows static methods to be called ❌ DO NOT define extension methods implementing a feature in namespaces normally associated with other features. Instead, define them in the namespace associated with the feature they belong to. - ❌ AVOID generic naming of namespaces dedicated to extension methods (for example, "Extensions"). Use a descriptive name (for example, "Routing") instead. + ❌ AVOID generic naming of namespaces dedicated to extension methods (e.g., "Extensions"). Use a descriptive name (e.g., "Routing") instead. *Portions © 2005, 2009 Microsoft Corporation. All rights reserved.* diff --git a/docs/standard/design-guidelines/guidelines-for-collections.md b/docs/standard/design-guidelines/guidelines-for-collections.md index 4367bfeadfcdd..cf5e83601d836 100644 --- a/docs/standard/design-guidelines/guidelines-for-collections.md +++ b/docs/standard/design-guidelines/guidelines-for-collections.md @@ -44,11 +44,11 @@ Any type designed specifically to manipulate a group of objects having some comm ✔️ DO use `Collection` or a subclass of `Collection` for properties or return values representing read/write collections. - If `Collection` does not meet some requirement (for example, the collection must not implement ), use a custom collection by implementing `IEnumerable`, `ICollection`, or . + If `Collection` does not meet some requirement (e.g., the collection must not implement ), use a custom collection by implementing `IEnumerable`, `ICollection`, or . ✔️ DO use , a subclass of `ReadOnlyCollection`, or in rare cases `IEnumerable` for properties or return values representing read-only collections. - In general, prefer `ReadOnlyCollection`. If it does not meet some requirement (for example, the collection must not implement `IList`), use a custom collection by implementing `IEnumerable`, `ICollection`, or `IList`. If you do implement a custom read-only collection, implement `ICollection.IsReadOnly` to return `true`. + In general, prefer `ReadOnlyCollection`. If it does not meet some requirement (e.g., the collection must not implement `IList`), use a custom collection by implementing `IEnumerable`, `ICollection`, or `IList`. If you do implement a custom read-only collection, implement `ICollection.IsReadOnly` to return `true`. In cases where you are sure that the only scenario you will ever want to support is forward-only iteration, you can simply use `IEnumerable`. @@ -78,7 +78,7 @@ Any type designed specifically to manipulate a group of objects having some comm ✔️ DO use either a snapshot collection or a live `IEnumerable` (or its subtype) to represent collections that are volatile (i.e., that can change without explicitly modifying the collection). - In general, all collections representing a shared resource (for example, files in a directory) are volatile. Such collections are very difficult or impossible to implement as live collections unless the implementation is simply a forward-only enumerator. + In general, all collections representing a shared resource (e.g., files in a directory) are volatile. Such collections are very difficult or impossible to implement as live collections unless the implementation is simply a forward-only enumerator. ## Choosing Between Arrays and Collections @@ -92,7 +92,7 @@ Any type designed specifically to manipulate a group of objects having some comm ✔️ DO use byte arrays instead of collections of bytes. - ❌ DO NOT use arrays for properties if the property would have to return a new array (for example, a copy of an internal array) every time the property getter is called. + ❌ DO NOT use arrays for properties if the property would have to return a new array (e.g., a copy of an internal array) every time the property getter is called. ## Implementing Custom Collections @@ -110,7 +110,7 @@ Any type designed specifically to manipulate a group of objects having some comm ### Naming Custom Collections - Collections (types that implement `IEnumerable`) are created mainly for two reasons: (1) to create a new data structure with structure-specific operations and often different performance characteristics than existing data structures (for example, , , ), and (2) to create a specialized collection for holding a specific set of items (for example, ). Data structures are most often used in the internal implementation of applications and libraries. Specialized collections are mainly to be exposed in APIs (as property and parameter types). + Collections (types that implement `IEnumerable`) are created mainly for two reasons: (1) to create a new data structure with structure-specific operations and often different performance characteristics than existing data structures (e.g., , , ), and (2) to create a specialized collection for holding a specific set of items (e.g., ). Data structures are most often used in the internal implementation of applications and libraries. Specialized collections are mainly to be exposed in APIs (as property and parameter types). ✔️ DO use the "Dictionary" suffix in names of abstractions implementing `IDictionary` or `IDictionary`. diff --git a/docs/standard/design-guidelines/names-of-classes-structs-and-interfaces.md b/docs/standard/design-guidelines/names-of-classes-structs-and-interfaces.md index fb7af7d697b9e..f5e88ce9fae7d 100644 --- a/docs/standard/design-guidelines/names-of-classes-structs-and-interfaces.md +++ b/docs/standard/design-guidelines/names-of-classes-structs-and-interfaces.md @@ -28,7 +28,7 @@ The naming guidelines that follow apply to general type naming. Nouns and noun phrases should be used rarely and they might indicate that the type should be an abstract class, and not an interface. - ❌ DO NOT give class names a prefix (for example, "C"). + ❌ DO NOT give class names a prefix (e.g., "C"). ✔️ CONSIDER ending the name of derived classes with the name of the base class. @@ -94,7 +94,7 @@ public interface ISessionChannel where TSession : ISession { ❌ DO NOT use "Flag" or "Flags" suffixes in enum type names. - ❌ DO NOT use a prefix on enumeration value names (for example, "ad" for ADO enums, "rtf" for rich text enums, etc.). + ❌ DO NOT use a prefix on enumeration value names (e.g., "ad" for ADO enums, "rtf" for rich text enums, etc.). *Portions © 2005, 2009 Microsoft Corporation. All rights reserved.* diff --git a/docs/standard/design-guidelines/names-of-namespaces.md b/docs/standard/design-guidelines/names-of-namespaces.md index 7a2f91afa9604..f89529e9bade8 100644 --- a/docs/standard/design-guidelines/names-of-namespaces.md +++ b/docs/standard/design-guidelines/names-of-namespaces.md @@ -29,7 +29,7 @@ As with other naming guidelines, the goal when naming namespaces is creating suf ❌ DO NOT use organizational hierarchies as the basis for names in namespace hierarchies, because group names within corporations tend to be short-lived. Organize the hierarchy of namespaces around groups of related technologies. - ✔️ DO use PascalCasing, and separate namespace components with periods (for example, `Microsoft.Office.PowerPoint`). If your brand employs nontraditional casing, you should follow the casing defined by your brand, even if it deviates from normal namespace casing. + ✔️ DO use PascalCasing, and separate namespace components with periods (e.g., `Microsoft.Office.PowerPoint`). If your brand employs nontraditional casing, you should follow the casing defined by your brand, even if it deviates from normal namespace casing. ✔️ CONSIDER using plural namespace names where appropriate. diff --git a/docs/standard/design-guidelines/property.md b/docs/standard/design-guidelines/property.md index fee64c97be6fb..37ec27513533d 100644 --- a/docs/standard/design-guidelines/property.md +++ b/docs/standard/design-guidelines/property.md @@ -53,7 +53,7 @@ Although properties are technically very similar to methods, they are quite diff If the design requires other types of parameters, strongly reevaluate whether the API really represents an accessor to a logical collection. If it does not, use a method. Consider starting the method name with `Get` or `Set`. - ✔️ DO use the name `Item` for indexed properties unless there is an obviously better name (for example, see the property on `System.String`). + ✔️ DO use the name `Item` for indexed properties unless there is an obviously better name (e.g., see the property on `System.String`). In C#, indexers are by default named Item. The can be used to customize this name. diff --git a/docs/standard/design-guidelines/usage-guidelines.md b/docs/standard/design-guidelines/usage-guidelines.md index 58f9a4efe2fcd..3b5ee7139ec7c 100644 --- a/docs/standard/design-guidelines/usage-guidelines.md +++ b/docs/standard/design-guidelines/usage-guidelines.md @@ -2,14 +2,14 @@ description: "Learn more about: Usage guidelines" title: "Usage guidelines" ms.date: "10/22/2008" -helpviewer_keywords: +helpviewer_keywords: - "class library design guidelines [.NET Framework], usage guidelines" ms.assetid: 42215ffa-a099-4a26-b14e-fb2bdb6f95b7 --- # Usage guidelines -This section contains guidelines for using common types in publicly accessible APIs. It deals with direct usage of built-in Framework types (for example, serialization attributes) and overloading common operators. - +This section contains guidelines for using common types in publicly accessible APIs. It deals with direct usage of built-in Framework types (e.g., serialization attributes) and overloading common operators. + The interface is not covered in this section, but is discussed in the [Dispose Pattern](dispose-pattern.md) section. > [!NOTE] @@ -27,7 +27,7 @@ The interface is not cove *Portions © 2005, 2009 Microsoft Corporation. All rights reserved.* *Reprinted by permission of Pearson Education, Inc. from [Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition](https://www.informit.com/store/framework-design-guidelines-conventions-idioms-and-9780321545619) by Krzysztof Cwalina and Brad Abrams, published Oct 22, 2008 by Addison-Wesley Professional as part of the Microsoft Windows Development Series.* - + ## See also - [Framework Design Guidelines](index.md) From 4b5b141735adf265bc52b8617c32b895b6bb3e10 Mon Sep 17 00:00:00 2001 From: Azure SDK Bot <53356347+azure-sdk@users.noreply.github.com> Date: Wed, 28 May 2025 06:04:27 -0700 Subject: [PATCH 03/13] Update package index with latest published versions (#46456) --- docs/azure/includes/dotnet-all.md | 10 +++++----- docs/azure/includes/dotnet-new.md | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/azure/includes/dotnet-all.md b/docs/azure/includes/dotnet-all.md index 4a285b379d2ba..8b2a75fe62ab4 100644 --- a/docs/azure/includes/dotnet-all.md +++ b/docs/azure/includes/dotnet-all.md @@ -62,7 +62,7 @@ | Key Vault - Keys | NuGet [4.7.0](https://www.nuget.org/packages/Azure.Security.KeyVault.Keys/4.7.0)
NuGet [4.8.0-beta.1](https://www.nuget.org/packages/Azure.Security.KeyVault.Keys/4.8.0-beta.1) | [docs](/dotnet/api/overview/azure/Security.KeyVault.Keys-readme) | GitHub [4.7.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Security.KeyVault.Keys_4.7.0/sdk/keyvault/Azure.Security.KeyVault.Keys/)
GitHub [4.8.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Security.KeyVault.Keys_4.8.0-beta.1/sdk/keyvault/Azure.Security.KeyVault.Keys/) | | Key Vault - Secrets | NuGet [4.7.0](https://www.nuget.org/packages/Azure.Security.KeyVault.Secrets/4.7.0)
NuGet [4.8.0-beta.1](https://www.nuget.org/packages/Azure.Security.KeyVault.Secrets/4.8.0-beta.1) | [docs](/dotnet/api/overview/azure/Security.KeyVault.Secrets-readme) | GitHub [4.7.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Security.KeyVault.Secrets_4.7.0/sdk/keyvault/Azure.Security.KeyVault.Secrets/)
GitHub [4.8.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Security.KeyVault.Secrets_4.8.0-beta.1/sdk/keyvault/Azure.Security.KeyVault.Secrets/) | | Language Text | NuGet [1.0.0-beta.2](https://www.nuget.org/packages/Azure.AI.Language.Text/1.0.0-beta.2) | [docs](/dotnet/api/overview/azure/AI.Language.Text-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.2](https://github.com/Azure/azure-sdk-for-net/tree/Azure.AI.Language.Text_1.0.0-beta.2/sdk/cognitivelanguage/Azure.AI.Language.Text/) | -| Load Testing | NuGet [1.0.2](https://www.nuget.org/packages/Azure.Developer.LoadTesting/1.0.2) | [docs](/dotnet/api/overview/azure/Developer.LoadTesting-readme) | GitHub [1.0.2](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Developer.LoadTesting_1.0.2/sdk/loadtestservice/Azure.Developer.LoadTesting/) | +| Load Testing | NuGet [1.0.2](https://www.nuget.org/packages/Azure.Developer.LoadTesting/1.0.2)
NuGet [1.2.0-beta.1](https://www.nuget.org/packages/Azure.Developer.LoadTesting/1.2.0-beta.1) | [docs](/dotnet/api/overview/azure/Developer.LoadTesting-readme) | GitHub [1.0.2](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Developer.LoadTesting_1.0.2/sdk/loadtestservice/Azure.Developer.LoadTesting/)
GitHub [1.2.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Developer.LoadTesting_1.2.0-beta.1/sdk/loadtestservice/Azure.Developer.LoadTesting/) | | Maps Common | NuGet [1.0.0-beta.4](https://www.nuget.org/packages/Azure.Maps.Common/1.0.0-beta.4) | [docs](/dotnet/api/overview/azure/Maps.Common-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.4](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Maps.Common_1.0.0-beta.4/sdk/maps/Azure.Maps.Common/) | | Maps Geolocation | NuGet [1.0.0-beta.3](https://www.nuget.org/packages/Azure.Maps.Geolocation/1.0.0-beta.3) | [docs](/dotnet/api/overview/azure/Maps.Geolocation-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.3](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Maps.Geolocation_1.0.0-beta.3/sdk/maps/Azure.Maps.Geolocation/) | | Maps Render | NuGet [2.0.0-beta.1](https://www.nuget.org/packages/Azure.Maps.Rendering/2.0.0-beta.1) | [docs](/dotnet/api/overview/azure/Maps.Rendering-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [2.0.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Maps.Rendering_2.0.0-beta.1/sdk/maps/Azure.Maps.Rendering/) | @@ -228,7 +228,7 @@ | Resource Management - Database Watcher | NuGet [1.0.0-beta.1](https://www.nuget.org/packages/Azure.ResourceManager.DatabaseWatcher/1.0.0-beta.1) | [docs](/dotnet/api/overview/azure/ResourceManager.DatabaseWatcher-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.DatabaseWatcher_1.0.0-beta.1/sdk/databasewatcher/Azure.ResourceManager.DatabaseWatcher/) | | Resource Management - Datadog | NuGet [1.0.0-beta.5](https://www.nuget.org/packages/Azure.ResourceManager.Datadog/1.0.0-beta.5) | [docs](/dotnet/api/overview/azure/ResourceManager.Datadog-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.5](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.Datadog_1.0.0-beta.5/sdk/datadog/Azure.ResourceManager.Datadog/) | | Resource Management - Defender EASM | NuGet [1.0.0-beta.3](https://www.nuget.org/packages/Azure.ResourceManager.DefenderEasm/1.0.0-beta.3) | [docs](/dotnet/api/overview/azure/ResourceManager.DefenderEasm-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.3](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.DefenderEasm_1.0.0-beta.3/sdk/defendereasm/Azure.ResourceManager.DefenderEasm/) | -| Resource Management - Dell.Storage | NuGet [1.0.0-beta.1](https://www.nuget.org/packages/Azure.ResourceManager.Dell.Storage/1.0.0-beta.1) | | GitHub [1.0.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.Dell.Storage_1.0.0-beta.1/sdk/dellstorage/Azure.ResourceManager.Dell.Storage/) | +| Resource Management - Dell.Storage | NuGet [1.0.0-beta.1](https://www.nuget.org/packages/Azure.ResourceManager.Dell.Storage/1.0.0-beta.1) | [docs](/dotnet/api/overview/azure/ResourceManager.Dell.Storage-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.Dell.Storage_1.0.0-beta.1/sdk/dellstorage/Azure.ResourceManager.Dell.Storage/) | | Resource Management - Dependencymap | NuGet [1.0.0-beta.1](https://www.nuget.org/packages/Azure.ResourceManager.DependencyMap/1.0.0-beta.1) | [docs](/dotnet/api/overview/azure/ResourceManager.DependencyMap-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.DependencyMap_1.0.0-beta.1/sdk/dependencymap/Azure.ResourceManager.DependencyMap/) | | Resource Management - Deployment Manager | NuGet [1.0.0-beta.3](https://www.nuget.org/packages/Azure.ResourceManager.DeploymentManager/1.0.0-beta.3) | [docs](/dotnet/api/overview/azure/ResourceManager.DeploymentManager-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.3](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.DeploymentManager_1.0.0-beta.3/sdk/deploymentmanager/Azure.ResourceManager.DeploymentManager/) | | Resource Management - Desktop Virtualization | NuGet [1.3.1](https://www.nuget.org/packages/Azure.ResourceManager.DesktopVirtualization/1.3.1) | [docs](/dotnet/api/overview/azure/ResourceManager.DesktopVirtualization-readme) | GitHub [1.3.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.DesktopVirtualization_1.3.1/sdk/desktopvirtualization/Azure.ResourceManager.DesktopVirtualization/) | @@ -310,7 +310,7 @@ | Resource Management - New Relic Observability | NuGet [1.1.1](https://www.nuget.org/packages/Azure.ResourceManager.NewRelicObservability/1.1.1) | [docs](/dotnet/api/overview/azure/ResourceManager.NewRelicObservability-readme) | GitHub [1.1.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.NewRelicObservability_1.1.1/sdk/newrelicobservability/Azure.ResourceManager.NewRelicObservability/) | | Resource Management - Nginx | NuGet [1.0.0](https://www.nuget.org/packages/Azure.ResourceManager.Nginx/1.0.0)
NuGet [1.1.0-beta.3](https://www.nuget.org/packages/Azure.ResourceManager.Nginx/1.1.0-beta.3) | [docs](/dotnet/api/overview/azure/ResourceManager.Nginx-readme) | GitHub [1.0.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.Nginx_1.0.0/sdk/nginx/Azure.ResourceManager.Nginx/)
GitHub [1.1.0-beta.3](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.Nginx_1.1.0-beta.3/sdk/nginx/Azure.ResourceManager.Nginx/) | | Resource Management - Notification Hubs | NuGet [1.1.1](https://www.nuget.org/packages/Azure.ResourceManager.NotificationHubs/1.1.1)
NuGet [1.2.0-beta.2](https://www.nuget.org/packages/Azure.ResourceManager.NotificationHubs/1.2.0-beta.2) | [docs](/dotnet/api/overview/azure/ResourceManager.NotificationHubs-readme) | GitHub [1.1.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.NotificationHubs_1.1.1/sdk/notificationhubs/Azure.ResourceManager.NotificationHubs/)
GitHub [1.2.0-beta.2](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.NotificationHubs_1.2.0-beta.2/sdk/notificationhubs/Azure.ResourceManager.NotificationHubs/) | -| Resource Management - Onlineexperimentation | NuGet [1.0.0-beta.1](https://www.nuget.org/packages/Azure.ResourceManager.OnlineExperimentation/1.0.0-beta.1) | | GitHub [1.0.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.OnlineExperimentation_1.0.0-beta.1/sdk/onlineexperimentation/Azure.ResourceManager.OnlineExperimentation/) | +| Resource Management - Onlineexperimentation | NuGet [1.0.0-beta.1](https://www.nuget.org/packages/Azure.ResourceManager.OnlineExperimentation/1.0.0-beta.1) | [docs](/dotnet/api/overview/azure/ResourceManager.OnlineExperimentation-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.OnlineExperimentation_1.0.0-beta.1/sdk/onlineexperimentation/Azure.ResourceManager.OnlineExperimentation/) | | Resource Management - Oracle Database | NuGet [1.0.1](https://www.nuget.org/packages/Azure.ResourceManager.OracleDatabase/1.0.1) | [docs](/dotnet/api/overview/azure/ResourceManager.OracleDatabase-readme) | GitHub [1.0.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.OracleDatabase_1.0.1/sdk/oracle/Azure.ResourceManager.OracleDatabase/) | | Resource Management - Orbital | NuGet [1.1.1](https://www.nuget.org/packages/Azure.ResourceManager.Orbital/1.1.1) | [docs](/dotnet/api/overview/azure/ResourceManager.Orbital-readme) | GitHub [1.1.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.Orbital_1.1.1/sdk/orbital/Azure.ResourceManager.Orbital/) | | Resource Management - Palo Alto Networks - Next Generation Firewall | NuGet [1.1.1](https://www.nuget.org/packages/Azure.ResourceManager.PaloAltoNetworks.Ngfw/1.1.1) | [docs](/dotnet/api/overview/azure/ResourceManager.PaloAltoNetworks.Ngfw-readme) | GitHub [1.1.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.PaloAltoNetworks.Ngfw_1.1.1/sdk/paloaltonetworks.ngfw/Azure.ResourceManager.PaloAltoNetworks.Ngfw/) | @@ -355,7 +355,7 @@ | Resource Management - Service Linker | NuGet [1.1.1](https://www.nuget.org/packages/Azure.ResourceManager.ServiceLinker/1.1.1) | [docs](/dotnet/api/overview/azure/ResourceManager.ServiceLinker-readme) | GitHub [1.1.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.ServiceLinker_1.1.1/sdk/servicelinker/Azure.ResourceManager.ServiceLinker/) | | Resource Management - Service Networking | NuGet [1.1.0](https://www.nuget.org/packages/Azure.ResourceManager.ServiceNetworking/1.1.0) | [docs](/dotnet/api/overview/azure/ResourceManager.ServiceNetworking-readme) | GitHub [1.1.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.ServiceNetworking_1.1.0/sdk/servicenetworking/Azure.ResourceManager.ServiceNetworking/) | | Resource Management - SignalR | NuGet [1.1.3](https://www.nuget.org/packages/Azure.ResourceManager.SignalR/1.1.3) | [docs](/dotnet/api/overview/azure/ResourceManager.SignalR-readme) | GitHub [1.1.3](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.SignalR_1.1.3/sdk/signalr/Azure.ResourceManager.SignalR/) | -| Resource Management - Sitemanager | NuGet [1.0.0-beta.1](https://www.nuget.org/packages/Azure.ResourceManager.SiteManager/1.0.0-beta.1) | | GitHub [1.0.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.SiteManager_1.0.0-beta.1/sdk/sitemanager/Azure.ResourceManager.SiteManager/) | +| Resource Management - Sitemanager | NuGet [1.0.0-beta.1](https://www.nuget.org/packages/Azure.ResourceManager.SiteManager/1.0.0-beta.1) | [docs](/dotnet/api/overview/azure/ResourceManager.SiteManager-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.SiteManager_1.0.0-beta.1/sdk/sitemanager/Azure.ResourceManager.SiteManager/) | | Resource Management - Sphere | NuGet [1.0.1](https://www.nuget.org/packages/Azure.ResourceManager.Sphere/1.0.1) | [docs](/dotnet/api/overview/azure/ResourceManager.Sphere-readme) | GitHub [1.0.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.Sphere_1.0.1/sdk/sphere/Azure.ResourceManager.Sphere/) | | Resource Management - Spring App Discovery | NuGet [1.0.0-beta.2](https://www.nuget.org/packages/Azure.ResourceManager.SpringAppDiscovery/1.0.0-beta.2) | [docs](/dotnet/api/overview/azure/ResourceManager.SpringAppDiscovery-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.2](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.SpringAppDiscovery_1.0.0-beta.2/sdk/springappdiscovery/Azure.ResourceManager.SpringAppDiscovery/) | | Resource Management - SQL | NuGet [1.3.0](https://www.nuget.org/packages/Azure.ResourceManager.Sql/1.3.0)
NuGet [1.4.0-beta.1](https://www.nuget.org/packages/Azure.ResourceManager.Sql/1.4.0-beta.1) | [docs](/dotnet/api/overview/azure/ResourceManager.Sql-readme) | GitHub [1.3.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.Sql_1.3.0/sdk/sqlmanagement/Azure.ResourceManager.Sql/)
GitHub [1.4.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.Sql_1.4.0-beta.1/sdk/sqlmanagement/Azure.ResourceManager.Sql/) | @@ -525,7 +525,7 @@ | Functions extension for IoT Edge | NuGet [1.0.7](https://www.nuget.org/packages/Microsoft.Azure.WebJobs.Extensions.EdgeHub/1.0.7) | | GitHub [1.0.7](https://github.com/Azure/iotedge/tree/1.0.7/edge-hub) | | Functions extension for Kafka | NuGet [4.1.1](https://www.nuget.org/packages/Microsoft.Azure.WebJobs.Extensions.Kafka/4.1.1) | | GitHub [4.1.1](https://github.com/Azure/azure-functions-kafka-extension/tree/3.0.0/src/Microsoft.Azure.WebJobs.Extensions.Kafka) | | Functions extension for Notification Hubs | NuGet [1.3.0](https://www.nuget.org/packages/Microsoft.Azure.WebJobs.Extensions.NotificationHubs/1.3.0) | | GitHub [1.3.0](https://github.com/Azure/azure-webjobs-sdk-extensions) | -| Functions extension for RabbitMQ | NuGet [2.0.4](https://www.nuget.org/packages/Microsoft.Azure.WebJobs.Extensions.RabbitMQ/2.0.4) | | GitHub [2.0.4](https://github.com/Azure/azure-functions-rabbitmq-extension/tree/v0.2.2029-beta) | +| Functions extension for RabbitMQ | NuGet [2.1.0](https://www.nuget.org/packages/Microsoft.Azure.WebJobs.Extensions.RabbitMQ/2.1.0) | | GitHub [2.1.0](https://github.com/Azure/azure-functions-rabbitmq-extension/tree/v0.2.2029-beta) | | Functions extension for script abstractions | NuGet [1.0.4-preview](https://www.nuget.org/packages/Microsoft.Azure.WebJobs.Script.Abstractions/1.0.4-preview) | | | | Functions extension for SendGrid | NuGet [3.1.0](https://www.nuget.org/packages/Microsoft.Azure.WebJobs.Extensions.SendGrid/3.1.0) | | GitHub [3.1.0](https://github.com/Azure/azure-webjobs-sdk-extensions/tree/v3.0.0/src/WebJobs.Extensions.SendGrid) | | Functions extension for Sources | NuGet [3.0.41](https://www.nuget.org/packages/Microsoft.Azure.WebJobs.Sources/3.0.41) | | GitHub [3.0.41](https://github.com/Azure/azure-webjobs-sdk) | diff --git a/docs/azure/includes/dotnet-new.md b/docs/azure/includes/dotnet-new.md index 2a3521a93a92b..eb8d5fb4efe64 100644 --- a/docs/azure/includes/dotnet-new.md +++ b/docs/azure/includes/dotnet-new.md @@ -63,7 +63,7 @@ | Key Vault - Keys | NuGet [4.7.0](https://www.nuget.org/packages/Azure.Security.KeyVault.Keys/4.7.0)
NuGet [4.8.0-beta.1](https://www.nuget.org/packages/Azure.Security.KeyVault.Keys/4.8.0-beta.1) | [docs](/dotnet/api/overview/azure/Security.KeyVault.Keys-readme) | GitHub [4.7.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Security.KeyVault.Keys_4.7.0/sdk/keyvault/Azure.Security.KeyVault.Keys/)
GitHub [4.8.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Security.KeyVault.Keys_4.8.0-beta.1/sdk/keyvault/Azure.Security.KeyVault.Keys/) | | Key Vault - Secrets | NuGet [4.7.0](https://www.nuget.org/packages/Azure.Security.KeyVault.Secrets/4.7.0)
NuGet [4.8.0-beta.1](https://www.nuget.org/packages/Azure.Security.KeyVault.Secrets/4.8.0-beta.1) | [docs](/dotnet/api/overview/azure/Security.KeyVault.Secrets-readme) | GitHub [4.7.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Security.KeyVault.Secrets_4.7.0/sdk/keyvault/Azure.Security.KeyVault.Secrets/)
GitHub [4.8.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Security.KeyVault.Secrets_4.8.0-beta.1/sdk/keyvault/Azure.Security.KeyVault.Secrets/) | | Language Text | NuGet [1.0.0-beta.2](https://www.nuget.org/packages/Azure.AI.Language.Text/1.0.0-beta.2) | [docs](/dotnet/api/overview/azure/AI.Language.Text-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.2](https://github.com/Azure/azure-sdk-for-net/tree/Azure.AI.Language.Text_1.0.0-beta.2/sdk/cognitivelanguage/Azure.AI.Language.Text/) | -| Load Testing | NuGet [1.0.2](https://www.nuget.org/packages/Azure.Developer.LoadTesting/1.0.2) | [docs](/dotnet/api/overview/azure/Developer.LoadTesting-readme) | GitHub [1.0.2](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Developer.LoadTesting_1.0.2/sdk/loadtestservice/Azure.Developer.LoadTesting/) | +| Load Testing | NuGet [1.0.2](https://www.nuget.org/packages/Azure.Developer.LoadTesting/1.0.2)
NuGet [1.2.0-beta.1](https://www.nuget.org/packages/Azure.Developer.LoadTesting/1.2.0-beta.1) | [docs](/dotnet/api/overview/azure/Developer.LoadTesting-readme) | GitHub [1.0.2](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Developer.LoadTesting_1.0.2/sdk/loadtestservice/Azure.Developer.LoadTesting/)
GitHub [1.2.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Developer.LoadTesting_1.2.0-beta.1/sdk/loadtestservice/Azure.Developer.LoadTesting/) | | Maps Common | NuGet [1.0.0-beta.4](https://www.nuget.org/packages/Azure.Maps.Common/1.0.0-beta.4) | [docs](/dotnet/api/overview/azure/Maps.Common-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.4](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Maps.Common_1.0.0-beta.4/sdk/maps/Azure.Maps.Common/) | | Maps Geolocation | NuGet [1.0.0-beta.3](https://www.nuget.org/packages/Azure.Maps.Geolocation/1.0.0-beta.3) | [docs](/dotnet/api/overview/azure/Maps.Geolocation-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.3](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Maps.Geolocation_1.0.0-beta.3/sdk/maps/Azure.Maps.Geolocation/) | | Maps Render | NuGet [2.0.0-beta.1](https://www.nuget.org/packages/Azure.Maps.Rendering/2.0.0-beta.1) | [docs](/dotnet/api/overview/azure/Maps.Rendering-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [2.0.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.Maps.Rendering_2.0.0-beta.1/sdk/maps/Azure.Maps.Rendering/) | @@ -234,7 +234,7 @@ | Resource Management - Database Watcher | NuGet [1.0.0-beta.1](https://www.nuget.org/packages/Azure.ResourceManager.DatabaseWatcher/1.0.0-beta.1) | [docs](/dotnet/api/overview/azure/ResourceManager.DatabaseWatcher-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.DatabaseWatcher_1.0.0-beta.1/sdk/databasewatcher/Azure.ResourceManager.DatabaseWatcher/) | | Resource Management - Datadog | NuGet [1.0.0-beta.5](https://www.nuget.org/packages/Azure.ResourceManager.Datadog/1.0.0-beta.5) | [docs](/dotnet/api/overview/azure/ResourceManager.Datadog-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.5](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.Datadog_1.0.0-beta.5/sdk/datadog/Azure.ResourceManager.Datadog/) | | Resource Management - Defender EASM | NuGet [1.0.0-beta.3](https://www.nuget.org/packages/Azure.ResourceManager.DefenderEasm/1.0.0-beta.3) | [docs](/dotnet/api/overview/azure/ResourceManager.DefenderEasm-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.3](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.DefenderEasm_1.0.0-beta.3/sdk/defendereasm/Azure.ResourceManager.DefenderEasm/) | -| Resource Management - Dell.Storage | NuGet [1.0.0-beta.1](https://www.nuget.org/packages/Azure.ResourceManager.Dell.Storage/1.0.0-beta.1) | | GitHub [1.0.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.Dell.Storage_1.0.0-beta.1/sdk/dellstorage/Azure.ResourceManager.Dell.Storage/) | +| Resource Management - Dell.Storage | NuGet [1.0.0-beta.1](https://www.nuget.org/packages/Azure.ResourceManager.Dell.Storage/1.0.0-beta.1) | [docs](/dotnet/api/overview/azure/ResourceManager.Dell.Storage-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.Dell.Storage_1.0.0-beta.1/sdk/dellstorage/Azure.ResourceManager.Dell.Storage/) | | Resource Management - Dependencymap | NuGet [1.0.0-beta.1](https://www.nuget.org/packages/Azure.ResourceManager.DependencyMap/1.0.0-beta.1) | [docs](/dotnet/api/overview/azure/ResourceManager.DependencyMap-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.DependencyMap_1.0.0-beta.1/sdk/dependencymap/Azure.ResourceManager.DependencyMap/) | | Resource Management - Deployment Manager | NuGet [1.0.0-beta.3](https://www.nuget.org/packages/Azure.ResourceManager.DeploymentManager/1.0.0-beta.3) | [docs](/dotnet/api/overview/azure/ResourceManager.DeploymentManager-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.3](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.DeploymentManager_1.0.0-beta.3/sdk/deploymentmanager/Azure.ResourceManager.DeploymentManager/) | | Resource Management - Desktop Virtualization | NuGet [1.3.1](https://www.nuget.org/packages/Azure.ResourceManager.DesktopVirtualization/1.3.1) | [docs](/dotnet/api/overview/azure/ResourceManager.DesktopVirtualization-readme) | GitHub [1.3.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.DesktopVirtualization_1.3.1/sdk/desktopvirtualization/Azure.ResourceManager.DesktopVirtualization/) | @@ -318,7 +318,7 @@ | Resource Management - New Relic Observability | NuGet [1.1.1](https://www.nuget.org/packages/Azure.ResourceManager.NewRelicObservability/1.1.1) | [docs](/dotnet/api/overview/azure/ResourceManager.NewRelicObservability-readme) | GitHub [1.1.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.NewRelicObservability_1.1.1/sdk/newrelicobservability/Azure.ResourceManager.NewRelicObservability/) | | Resource Management - Nginx | NuGet [1.0.0](https://www.nuget.org/packages/Azure.ResourceManager.Nginx/1.0.0)
NuGet [1.1.0-beta.3](https://www.nuget.org/packages/Azure.ResourceManager.Nginx/1.1.0-beta.3) | [docs](/dotnet/api/overview/azure/ResourceManager.Nginx-readme) | GitHub [1.0.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.Nginx_1.0.0/sdk/nginx/Azure.ResourceManager.Nginx/)
GitHub [1.1.0-beta.3](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.Nginx_1.1.0-beta.3/sdk/nginx/Azure.ResourceManager.Nginx/) | | Resource Management - Notification Hubs | NuGet [1.1.1](https://www.nuget.org/packages/Azure.ResourceManager.NotificationHubs/1.1.1)
NuGet [1.2.0-beta.2](https://www.nuget.org/packages/Azure.ResourceManager.NotificationHubs/1.2.0-beta.2) | [docs](/dotnet/api/overview/azure/ResourceManager.NotificationHubs-readme) | GitHub [1.1.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.NotificationHubs_1.1.1/sdk/notificationhubs/Azure.ResourceManager.NotificationHubs/)
GitHub [1.2.0-beta.2](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.NotificationHubs_1.2.0-beta.2/sdk/notificationhubs/Azure.ResourceManager.NotificationHubs/) | -| Resource Management - Onlineexperimentation | NuGet [1.0.0-beta.1](https://www.nuget.org/packages/Azure.ResourceManager.OnlineExperimentation/1.0.0-beta.1) | | GitHub [1.0.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.OnlineExperimentation_1.0.0-beta.1/sdk/onlineexperimentation/Azure.ResourceManager.OnlineExperimentation/) | +| Resource Management - Onlineexperimentation | NuGet [1.0.0-beta.1](https://www.nuget.org/packages/Azure.ResourceManager.OnlineExperimentation/1.0.0-beta.1) | [docs](/dotnet/api/overview/azure/ResourceManager.OnlineExperimentation-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.OnlineExperimentation_1.0.0-beta.1/sdk/onlineexperimentation/Azure.ResourceManager.OnlineExperimentation/) | | Resource Management - Oracle Database | NuGet [1.0.1](https://www.nuget.org/packages/Azure.ResourceManager.OracleDatabase/1.0.1) | [docs](/dotnet/api/overview/azure/ResourceManager.OracleDatabase-readme) | GitHub [1.0.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.OracleDatabase_1.0.1/sdk/oracle/Azure.ResourceManager.OracleDatabase/) | | Resource Management - Orbital | NuGet [1.1.1](https://www.nuget.org/packages/Azure.ResourceManager.Orbital/1.1.1) | [docs](/dotnet/api/overview/azure/ResourceManager.Orbital-readme) | GitHub [1.1.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.Orbital_1.1.1/sdk/orbital/Azure.ResourceManager.Orbital/) | | Resource Management - Palo Alto Networks - Next Generation Firewall | NuGet [1.1.1](https://www.nuget.org/packages/Azure.ResourceManager.PaloAltoNetworks.Ngfw/1.1.1) | [docs](/dotnet/api/overview/azure/ResourceManager.PaloAltoNetworks.Ngfw-readme) | GitHub [1.1.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.PaloAltoNetworks.Ngfw_1.1.1/sdk/paloaltonetworks.ngfw/Azure.ResourceManager.PaloAltoNetworks.Ngfw/) | @@ -363,7 +363,7 @@ | Resource Management - Service Linker | NuGet [1.1.1](https://www.nuget.org/packages/Azure.ResourceManager.ServiceLinker/1.1.1) | [docs](/dotnet/api/overview/azure/ResourceManager.ServiceLinker-readme) | GitHub [1.1.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.ServiceLinker_1.1.1/sdk/servicelinker/Azure.ResourceManager.ServiceLinker/) | | Resource Management - Service Networking | NuGet [1.1.0](https://www.nuget.org/packages/Azure.ResourceManager.ServiceNetworking/1.1.0) | [docs](/dotnet/api/overview/azure/ResourceManager.ServiceNetworking-readme) | GitHub [1.1.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.ServiceNetworking_1.1.0/sdk/servicenetworking/Azure.ResourceManager.ServiceNetworking/) | | Resource Management - SignalR | NuGet [1.1.3](https://www.nuget.org/packages/Azure.ResourceManager.SignalR/1.1.3) | [docs](/dotnet/api/overview/azure/ResourceManager.SignalR-readme) | GitHub [1.1.3](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.SignalR_1.1.3/sdk/signalr/Azure.ResourceManager.SignalR/) | -| Resource Management - Sitemanager | NuGet [1.0.0-beta.1](https://www.nuget.org/packages/Azure.ResourceManager.SiteManager/1.0.0-beta.1) | | GitHub [1.0.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.SiteManager_1.0.0-beta.1/sdk/sitemanager/Azure.ResourceManager.SiteManager/) | +| Resource Management - Sitemanager | NuGet [1.0.0-beta.1](https://www.nuget.org/packages/Azure.ResourceManager.SiteManager/1.0.0-beta.1) | [docs](/dotnet/api/overview/azure/ResourceManager.SiteManager-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.SiteManager_1.0.0-beta.1/sdk/sitemanager/Azure.ResourceManager.SiteManager/) | | Resource Management - Sphere | NuGet [1.0.1](https://www.nuget.org/packages/Azure.ResourceManager.Sphere/1.0.1) | [docs](/dotnet/api/overview/azure/ResourceManager.Sphere-readme) | GitHub [1.0.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.Sphere_1.0.1/sdk/sphere/Azure.ResourceManager.Sphere/) | | Resource Management - Spring App Discovery | NuGet [1.0.0-beta.2](https://www.nuget.org/packages/Azure.ResourceManager.SpringAppDiscovery/1.0.0-beta.2) | [docs](/dotnet/api/overview/azure/ResourceManager.SpringAppDiscovery-readme?view=azure-dotnet-preview&preserve-view=true) | GitHub [1.0.0-beta.2](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.SpringAppDiscovery_1.0.0-beta.2/sdk/springappdiscovery/Azure.ResourceManager.SpringAppDiscovery/) | | Resource Management - SQL | NuGet [1.3.0](https://www.nuget.org/packages/Azure.ResourceManager.Sql/1.3.0)
NuGet [1.4.0-beta.1](https://www.nuget.org/packages/Azure.ResourceManager.Sql/1.4.0-beta.1) | [docs](/dotnet/api/overview/azure/ResourceManager.Sql-readme) | GitHub [1.3.0](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.Sql_1.3.0/sdk/sqlmanagement/Azure.ResourceManager.Sql/)
GitHub [1.4.0-beta.1](https://github.com/Azure/azure-sdk-for-net/tree/Azure.ResourceManager.Sql_1.4.0-beta.1/sdk/sqlmanagement/Azure.ResourceManager.Sql/) | From 4acbe1eca58545d0bbb3e74aea96ce0c85b2fcc5 Mon Sep 17 00:00:00 2001 From: David Pine Date: Wed, 28 May 2025 09:02:16 -0500 Subject: [PATCH 04/13] Fixes #45617 (#46459) --- docs/core/extensions/dependency-injection.md | 18 ++++++++++++------ docs/core/extensions/scoped-service.md | 4 ++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/core/extensions/dependency-injection.md b/docs/core/extensions/dependency-injection.md index 14f710d912ea0..62b3dee84cf4f 100644 --- a/docs/core/extensions/dependency-injection.md +++ b/docs/core/extensions/dependency-injection.md @@ -248,13 +248,19 @@ For web applications, a scoped lifetime indicates that services are created once In apps that process requests, scoped services are disposed at the end of the request. -When using Entity Framework Core, the extension method registers `DbContext` types with a scoped lifetime by default. - > [!NOTE] -> Do ***not*** resolve a scoped service from a singleton and be careful not to do so indirectly, for example, through a transient service. It may cause the service to have incorrect state when processing subsequent requests. It's fine to: -> -> - Resolve a singleton service from a scoped or transient service. -> - Resolve a scoped service from another scoped or transient service. +> When using Entity Framework Core, the extension method registers `DbContext` types with a scoped lifetime by default. + +A scoped service should always be used from within a scope—either an implicit scope (such as ASP.NET Core's per-request scope) or an explicit scope created with . + +Do ***not*** resolve a scoped service directly from a singleton using constructor injection or by requesting it from in the singleton. Doing so causes the scoped service to behave like a singleton, which can lead to incorrect state when processing subsequent requests. + +It's acceptable to resolve a scoped service within a singleton if you create and use an explicit scope with . + +It's also fine to: + +- Resolve a singleton service from a scoped or transient service. +- Resolve a scoped service from another scoped or transient service. By default, in the development environment, resolving a service from another service with a longer lifetime throws an exception. For more information, see [Scope validation](#scope-validation). diff --git a/docs/core/extensions/scoped-service.md b/docs/core/extensions/scoped-service.md index 8a8bc48818da1..dfa46e3f00be9 100644 --- a/docs/core/extensions/scoped-service.md +++ b/docs/core/extensions/scoped-service.md @@ -3,7 +3,7 @@ title: Use scoped services within a BackgroundService description: Learn how to use scoped services within a BackgroundService in .NET. author: IEvangelist ms.author: dapine -ms.date: 11/06/2024 +ms.date: 05/27/2025 ms.topic: tutorial --- @@ -15,7 +15,7 @@ In this tutorial, you learn how to: > [!div class="checklist"] > -> - Resolve scoped dependencies in a singleton . +> - Correctly resolve scoped dependencies in a singleton . > - Delegate work to a scoped service. > - Implement an `override` of . From eb0134add49d490357fd848508fe3adc82b8cadf Mon Sep 17 00:00:00 2001 From: lchaoer <1335717080@qq.com> Date: Wed, 28 May 2025 22:40:13 +0800 Subject: [PATCH 05/13] Update http-resilience.md (#46361) * Update http-resilience.md Add a warning of using ShouldHandle in retry strategy, this is a lesson in our real practice, hope we can help more people. * Apply suggestions from code review --------- Co-authored-by: David Pine --- docs/core/resilience/http-resilience.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/core/resilience/http-resilience.md b/docs/core/resilience/http-resilience.md index b2a9179432c7e..da8c78bb57442 100644 --- a/docs/core/resilience/http-resilience.md +++ b/docs/core/resilience/http-resilience.md @@ -203,6 +203,9 @@ The preceding code: There are many options available for each of the resilience strategies. For more information, see the [Polly docs: Strategies](https://www.pollydocs.org/strategies). For more information about configuring `ShouldHandle` delegates, see [Polly docs: Fault handling in reactive strategies](https://www.pollydocs.org/strategies#fault-handling). +> [!WARNING] +> If you're using both retry and timeout strategies, and you want to configure the `ShouldHandle` delegate in your retry strategy, make sure to consider whether it should handle Polly's timeout exception. Polly throws a `TimeoutRejectedException` (which inherits from ), not the standard . + ### Dynamic reload Polly supports dynamic reloading of the configured resilience strategies. This means that you can change the configuration of the resilience strategies at run time. To enable dynamic reload, use the appropriate `AddResilienceHandler` overload that exposes the `ResilienceHandlerContext`. Given the context, call `EnableReloads` of the corresponding resilience strategy options: From 9454e17c3dbb42c6f149e71cf3d5ab99c57ffeff Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Wed, 28 May 2025 08:49:55 -0700 Subject: [PATCH 06/13] Use OllamaSharp package (#46455) --- docs/ai/microsoft-extensions-ai.md | 9 +++------ docs/ai/quickstarts/chat-local-model.md | 4 ++-- docs/ai/quickstarts/snippets/local-ai/Program.cs | 3 ++- docs/ai/quickstarts/snippets/local-ai/ollama.csproj | 2 +- .../ConsoleAI.CacheResponses.csproj | 2 +- .../ConsoleAI.CacheResponses/Program.cs | 3 ++- .../ConsoleAI.ConsumeClientMiddleware.csproj | 1 - .../ConsoleAI.CreateEmbeddings/Program.cs | 2 +- .../ConsoleAI.CustomClientMiddle.csproj | 2 +- .../ConsoleAI.CustomClientMiddle/Program.cs | 3 ++- .../ConsoleAI.DependencyInjection.csproj | 2 +- .../ConsoleAI.DependencyInjection/Program.cs | 3 ++- .../ConsoleAI.FunctionalityPipelines.csproj | 2 +- .../ConsoleAI.FunctionalityPipelines/Program.cs | 3 ++- .../ConsoleAI.ProvideOptions.csproj | 2 +- .../ConsoleAI.ProvideOptions/Program.cs | 6 ++++-- .../ConsoleAI.StatelessStateful.csproj | 2 +- .../ConsoleAI.ToolCalling/ConsoleAI.ToolCalling.csproj | 2 +- .../ConsoleAI.ToolCalling/Program.cs | 7 +++++-- .../ConsoleAI.UseExample/ConsoleAI.UseExample.csproj | 2 +- .../ConsoleAI.UseExample/Program.cs | 7 +++++-- 21 files changed, 39 insertions(+), 30 deletions(-) diff --git a/docs/ai/microsoft-extensions-ai.md b/docs/ai/microsoft-extensions-ai.md index 1271a72ccad28..d2e3521ea75fe 100644 --- a/docs/ai/microsoft-extensions-ai.md +++ b/docs/ai/microsoft-extensions-ai.md @@ -90,14 +90,14 @@ Some models and services support _tool calling_. To gather additional informatio - : Provides factory methods for creating `AIFunction` instances that represent .NET methods. - : Wraps an `IChatClient` as another `IChatClient` that adds automatic function-invocation capabilities. -The following example demonstrates a random function invocation (this example depends on the [📦 Microsoft.Extensions.AI.Ollama](https://www.nuget.org/packages/Microsoft.Extensions.AI.Ollama) NuGet package): +The following example demonstrates a random function invocation (this example depends on the [📦 OllamaSharp](https://www.nuget.org/packages/OllamaSharp) NuGet package): :::code language="csharp" source="snippets/microsoft-extensions-ai/ConsoleAI.ToolCalling/Program.cs"::: The preceding code: - Defines a function named `GetCurrentWeather` that returns a random weather forecast. -- Instantiates a with an and configures it to use function invocation. +- Instantiates a with an `OllamaSharp.OllamaApiClient` and configures it to use function invocation. - Calls `GetStreamingResponseAsync` on the client, passing a prompt and a list of tools that includes a function created with . - Iterates over the response, printing each update to the console. @@ -213,10 +213,7 @@ The preceding code: - Has a primary constructor that accepts an endpoint and model ID, which are used to identify the generator. - Implements the `GenerateAsync` method to generate embeddings for a collection of input values. -The sample implementation just generates random embedding vectors. You can find actual concrete implementations in the following packages: - -- [📦 Microsoft.Extensions.AI.OpenAI](https://www.nuget.org/packages/Microsoft.Extensions.AI.OpenAI) -- [📦 Microsoft.Extensions.AI.Ollama](https://www.nuget.org/packages/Microsoft.Extensions.AI.Ollama) +The sample implementation just generates random embedding vectors. You can find a concrete implementation in the [📦 Microsoft.Extensions.AI.OpenAI](https://www.nuget.org/packages/Microsoft.Extensions.AI.OpenAI) package. #### Create embeddings diff --git a/docs/ai/quickstarts/chat-local-model.md b/docs/ai/quickstarts/chat-local-model.md index 46ef0e54c8c16..f9a81851dc766 100644 --- a/docs/ai/quickstarts/chat-local-model.md +++ b/docs/ai/quickstarts/chat-local-model.md @@ -62,10 +62,10 @@ Complete the following steps to create a .NET console app that connects to your dotnet new console -o LocalAI ``` -1. Add the [Microsoft.Extensions.AI.Ollama](https://www.nuget.org/packages/Microsoft.Extensions.AI.Ollama/) package to your app: +1. Add the [OllamaSharp](https://www.nuget.org/packages/OllamaSharp) package to your app: ```dotnetcli - dotnet add package Microsoft.Extensions.AI.Ollama --prerelease + dotnet add package OllamaSharp ``` 1. Open the new app in your editor of choice, such as Visual Studio Code. diff --git a/docs/ai/quickstarts/snippets/local-ai/Program.cs b/docs/ai/quickstarts/snippets/local-ai/Program.cs index 462240636407d..6fa248167a630 100644 --- a/docs/ai/quickstarts/snippets/local-ai/Program.cs +++ b/docs/ai/quickstarts/snippets/local-ai/Program.cs @@ -1,7 +1,8 @@ using Microsoft.Extensions.AI; +using OllamaSharp; IChatClient chatClient = - new OllamaChatClient(new Uri("http://localhost:11434/"), "phi3:mini"); + new OllamaApiClient(new Uri("http://localhost:11434/"), "phi3:mini"); // Start the conversation with context for the AI model List chatHistory = new(); diff --git a/docs/ai/quickstarts/snippets/local-ai/ollama.csproj b/docs/ai/quickstarts/snippets/local-ai/ollama.csproj index 72769923af101..009e31d504ddb 100644 --- a/docs/ai/quickstarts/snippets/local-ai/ollama.csproj +++ b/docs/ai/quickstarts/snippets/local-ai/ollama.csproj @@ -8,7 +8,7 @@ - + diff --git a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CacheResponses/ConsoleAI.CacheResponses.csproj b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CacheResponses/ConsoleAI.CacheResponses.csproj index d3eac63d4f11d..520b1df4b4feb 100644 --- a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CacheResponses/ConsoleAI.CacheResponses.csproj +++ b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CacheResponses/ConsoleAI.CacheResponses.csproj @@ -8,7 +8,7 @@ - + diff --git a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CacheResponses/Program.cs b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CacheResponses/Program.cs index 6e15bf3da0788..9466b20cd1953 100644 --- a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CacheResponses/Program.cs +++ b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CacheResponses/Program.cs @@ -2,8 +2,9 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +using OllamaSharp; -var sampleChatClient = new OllamaChatClient(new Uri("http://localhost:11434"), "llama3.1"); +var sampleChatClient = new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.1"); IChatClient client = new ChatClientBuilder(sampleChatClient) .UseDistributedCache(new MemoryDistributedCache( diff --git a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ConsumeClientMiddleware/ConsoleAI.ConsumeClientMiddleware.csproj b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ConsumeClientMiddleware/ConsoleAI.ConsumeClientMiddleware.csproj index eec4c52e0697b..bec56340237d7 100644 --- a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ConsumeClientMiddleware/ConsoleAI.ConsumeClientMiddleware.csproj +++ b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ConsumeClientMiddleware/ConsoleAI.ConsumeClientMiddleware.csproj @@ -8,7 +8,6 @@ - diff --git a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CreateEmbeddings/Program.cs b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CreateEmbeddings/Program.cs index a9b9c3adacc34..dfe3a9e5da91d 100644 --- a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CreateEmbeddings/Program.cs +++ b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CreateEmbeddings/Program.cs @@ -13,5 +13,5 @@ await generator.GenerateAsync(["What is AI?", "What is .NET?"])) // // -ReadOnlyMemory vector = await generator.GenerateEmbeddingVectorAsync("What is AI?"); +ReadOnlyMemory vector = await generator.GenerateVectorAsync("What is AI?"); // diff --git a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CustomClientMiddle/ConsoleAI.CustomClientMiddle.csproj b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CustomClientMiddle/ConsoleAI.CustomClientMiddle.csproj index a2b55209ee7a3..16005c3bde4c2 100644 --- a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CustomClientMiddle/ConsoleAI.CustomClientMiddle.csproj +++ b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CustomClientMiddle/ConsoleAI.CustomClientMiddle.csproj @@ -8,7 +8,7 @@ - + diff --git a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CustomClientMiddle/Program.cs b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CustomClientMiddle/Program.cs index bc71d9bb9e897..ccf9ca75f2621 100644 --- a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CustomClientMiddle/Program.cs +++ b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.CustomClientMiddle/Program.cs @@ -1,8 +1,9 @@ using Microsoft.Extensions.AI; +using OllamaSharp; using System.Threading.RateLimiting; var client = new RateLimitingChatClient( - new OllamaChatClient(new Uri("http://localhost:11434"), "llama3.1"), + new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.1"), new ConcurrencyLimiter(new() { PermitLimit = 1, QueueLimit = int.MaxValue })); Console.WriteLine(await client.GetResponseAsync("What color is the sky?")); diff --git a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.DependencyInjection/ConsoleAI.DependencyInjection.csproj b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.DependencyInjection/ConsoleAI.DependencyInjection.csproj index 237efaa795212..3e7966f8b04fa 100644 --- a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.DependencyInjection/ConsoleAI.DependencyInjection.csproj +++ b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.DependencyInjection/ConsoleAI.DependencyInjection.csproj @@ -8,7 +8,7 @@ - + diff --git a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.DependencyInjection/Program.cs b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.DependencyInjection/Program.cs index 67b58783b56fa..051e15709095a 100644 --- a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.DependencyInjection/Program.cs +++ b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.DependencyInjection/Program.cs @@ -1,11 +1,12 @@ using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using OllamaSharp; // App setup. var builder = Host.CreateApplicationBuilder(); builder.Services.AddDistributedMemoryCache(); -builder.Services.AddChatClient(new OllamaChatClient(new Uri("http://localhost:11434"), "llama3.1")) +builder.Services.AddChatClient(new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.1")) .UseDistributedCache(); var host = builder.Build(); diff --git a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.FunctionalityPipelines/ConsoleAI.FunctionalityPipelines.csproj b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.FunctionalityPipelines/ConsoleAI.FunctionalityPipelines.csproj index a1dfb4ddd3ab7..cb1871d662c75 100644 --- a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.FunctionalityPipelines/ConsoleAI.FunctionalityPipelines.csproj +++ b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.FunctionalityPipelines/ConsoleAI.FunctionalityPipelines.csproj @@ -9,7 +9,7 @@ - + diff --git a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.FunctionalityPipelines/Program.cs b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.FunctionalityPipelines/Program.cs index 9d9c913d0d481..6f5b2d4d1e5b5 100644 --- a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.FunctionalityPipelines/Program.cs +++ b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.FunctionalityPipelines/Program.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +using OllamaSharp; using OpenTelemetry.Trace; // Configure OpenTelemetry exporter. @@ -13,7 +14,7 @@ // // Explore changing the order of the intermediate "Use" calls. -IChatClient client = new ChatClientBuilder(new OllamaChatClient(new Uri("http://localhost:11434"), "llama3.1")) +IChatClient client = new ChatClientBuilder(new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.1")) .UseDistributedCache(new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions()))) .UseFunctionInvocation() .UseOpenTelemetry(sourceName: sourceName, configure: c => c.EnableSensitiveData = true) diff --git a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ProvideOptions/ConsoleAI.ProvideOptions.csproj b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ProvideOptions/ConsoleAI.ProvideOptions.csproj index f9bbd4a47b53b..6e0c682243e90 100644 --- a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ProvideOptions/ConsoleAI.ProvideOptions.csproj +++ b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ProvideOptions/ConsoleAI.ProvideOptions.csproj @@ -8,7 +8,7 @@ - + diff --git a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ProvideOptions/Program.cs b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ProvideOptions/Program.cs index bbf1bf5c385e2..8a4414e8df37d 100644 --- a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ProvideOptions/Program.cs +++ b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ProvideOptions/Program.cs @@ -1,7 +1,9 @@ using Microsoft.Extensions.AI; +using OllamaSharp; -IChatClient client = new OllamaChatClient(new Uri("http://localhost:11434")) - .AsBuilder() +IChatClient client = new OllamaApiClient(new Uri("http://localhost:11434")); + +client = ChatClientBuilderChatClientExtensions.AsBuilder(client) .ConfigureOptions(options => options.ModelId ??= "phi3") .Build(); diff --git a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.StatelessStateful/ConsoleAI.StatelessStateful.csproj b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.StatelessStateful/ConsoleAI.StatelessStateful.csproj index 4d4d6c15bd6ba..6e861d87b1e04 100644 --- a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.StatelessStateful/ConsoleAI.StatelessStateful.csproj +++ b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.StatelessStateful/ConsoleAI.StatelessStateful.csproj @@ -8,7 +8,7 @@ - + diff --git a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ToolCalling/ConsoleAI.ToolCalling.csproj b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ToolCalling/ConsoleAI.ToolCalling.csproj index f9bbd4a47b53b..6e0c682243e90 100644 --- a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ToolCalling/ConsoleAI.ToolCalling.csproj +++ b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ToolCalling/ConsoleAI.ToolCalling.csproj @@ -8,7 +8,7 @@ - + diff --git a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ToolCalling/Program.cs b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ToolCalling/Program.cs index 438cd7a4bd7dd..fe6a02031519c 100644 --- a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ToolCalling/Program.cs +++ b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.ToolCalling/Program.cs @@ -1,9 +1,12 @@ using Microsoft.Extensions.AI; +using OllamaSharp; string GetCurrentWeather() => Random.Shared.NextDouble() > 0.5 ? "It's sunny" : "It's raining"; -IChatClient client = new OllamaChatClient(new Uri("http://localhost:11434"), "llama3.1") - .AsBuilder() +IChatClient client = new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.1"); + +client = ChatClientBuilderChatClientExtensions + .AsBuilder(client) .UseFunctionInvocation() .Build(); diff --git a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.UseExample/ConsoleAI.UseExample.csproj b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.UseExample/ConsoleAI.UseExample.csproj index f9bbd4a47b53b..6e0c682243e90 100644 --- a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.UseExample/ConsoleAI.UseExample.csproj +++ b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.UseExample/ConsoleAI.UseExample.csproj @@ -8,7 +8,7 @@ - + diff --git a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.UseExample/Program.cs b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.UseExample/Program.cs index 333d9290e31f9..b8a6f9131a890 100644 --- a/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.UseExample/Program.cs +++ b/docs/ai/snippets/microsoft-extensions-ai/ConsoleAI.UseExample/Program.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.AI; +using OllamaSharp; using System.Threading.RateLimiting; RateLimiter rateLimiter = new ConcurrencyLimiter(new() @@ -7,8 +8,10 @@ QueueLimit = int.MaxValue }); -IChatClient client = new OllamaChatClient(new Uri("http://localhost:11434"), "llama3.1") - .AsBuilder() +IChatClient client = new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.1"); + +client = ChatClientBuilderChatClientExtensions + .AsBuilder(client) .UseDistributedCache() .Use(async (messages, options, nextAsync, cancellationToken) => { From dcc8d06602a9312280e755ab106d3ffc42a22f0e Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Wed, 28 May 2025 09:44:17 -0700 Subject: [PATCH 07/13] Align Orleans docs with .NET style guide (#46384) * Align Orleans docs with .NET style guide * Try to fix markdownlint spacing errors * Try to fix more markdownlint errors * Try to fix more markdownlint errors * Try to fix more markdownlint errors * Fixes * Fixes * Fixes * Fixes * Fixes * Fixes * Fixes * Fixes * Remove ms.service: orleans * review feedback * Update docs/orleans/benefits.md Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com> * Fixes * Fixes * another pass * another pass * undo * undo * Remove trailing space * Fix build errors * Fix build errors * Number ordered list items all as "1." instead of "1.", "2.", etc. --------- Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com> --- docs/orleans/benefits.md | 46 +-- docs/orleans/deployment/consul-deployment.md | 76 ++--- .../deployment/deploy-to-azure-app-service.md | 112 ++++---- .../deploy-to-azure-container-apps.md | 78 +++--- docs/orleans/deployment/docker-deployment.md | 122 ++++---- docs/orleans/deployment/handling-failures.md | 78 +++--- .../deployment/create-azure-resources.md | 4 +- .../deployment/create-github-secret.md | 4 +- .../deployment/create-service-principal.md | 4 +- docs/orleans/deployment/index.md | 17 +- docs/orleans/deployment/kubernetes.md | 71 ++--- .../global-single-instance.md | 19 +- .../multi-cluster-support/gossip-channels.md | 44 +-- .../multi-cluster-configuration.md | 59 ++-- .../multi-cluster-support/overview.md | 24 +- .../silo-configuration.md | 21 +- docs/orleans/deployment/service-fabric.md | 50 ++-- ...ooting-azure-cloud-services-deployments.md | 56 ++-- .../deployment/troubleshooting-deployments.md | 56 ++-- docs/orleans/grains/cancellation-tokens.md | 25 +- docs/orleans/grains/code-generation.md | 62 ++--- .../event-sourcing-configuration.md | 25 +- .../immediate-vs-delayed-confirmation.md | 37 +-- docs/orleans/grains/event-sourcing/index.md | 21 +- .../event-sourcing/journaledgrain-basics.md | 51 ++-- .../journaledgrain-diagnostics.md | 15 +- .../log-consistency-providers.md | 35 +-- .../grains/event-sourcing/notifications.md | 16 +- .../event-sourcing/replicated-instances.md | 33 +-- .../grains/external-tasks-and-grains.md | 51 ++-- docs/orleans/grains/grain-extensions.md | 37 +-- docs/orleans/grains/grain-identity.md | 55 ++-- docs/orleans/grains/grain-lifecycle.md | 33 +-- .../grain-persistence/azure-cosmos-db.md | 8 +- .../grains/grain-persistence/azure-storage.md | 13 +- .../grain-persistence/dynamodb-storage.md | 15 +- .../orleans/grains/grain-persistence/index.md | 95 +++---- .../grain-persistence/relational-storage.md | 127 ++++----- docs/orleans/grains/grain-placement.md | 70 ++--- docs/orleans/grains/grain-references.md | 47 ++-- .../backward-compatibility-guidelines.md | 25 +- .../grain-versioning/compatible-grains.md | 41 ++- .../deploying-new-versions-of-grains.md | 40 ++- .../grain-versioning/grain-versioning.md | 23 +- .../version-selector-strategy.md | 28 +- docs/orleans/grains/grainservices.md | 57 ++-- docs/orleans/grains/index.md | 39 +-- docs/orleans/grains/interceptors.md | 70 ++--- docs/orleans/grains/observers.md | 43 ++- docs/orleans/grains/oneway.md | 11 +- docs/orleans/grains/request-context.md | 29 +- docs/orleans/grains/request-scheduling.md | 71 ++--- .../orleans/grains/stateless-worker-grains.md | 42 ++- docs/orleans/grains/timers-and-reminders.md | 91 +++--- docs/orleans/grains/transactions.md | 45 +-- docs/orleans/host/client.md | 85 +++--- .../activation-collection.md | 59 ++-- .../adonet-configuration.md | 5 +- .../client-configuration.md | 25 +- .../configuring-ado-dot-net-providers.md | 13 +- .../configuring-garbage-collection.md | 13 +- .../orleans/host/configuration-guide/index.md | 13 +- .../list-of-options-classes.md | 21 +- .../local-development-configuration.md | 41 ++- .../serialization-configuration.md | 21 +- .../serialization-customization.md | 12 +- .../serialization-immutability.md | 36 +-- .../host/configuration-guide/serialization.md | 130 ++++----- .../server-configuration.md | 94 +++---- .../shutting-down-orleans.md | 11 +- .../host/configuration-guide/startup-tasks.md | 40 +-- .../typical-configurations.md | 13 +- docs/orleans/host/grain-directory.md | 23 +- docs/orleans/host/heterogeneous-silos.md | 33 +-- .../client-error-code-monitoring.md | 2 +- docs/orleans/host/monitoring/index.md | 73 ++--- .../monitoring/silo-error-code-monitoring.md | 2 +- docs/orleans/host/powershell-client.md | 51 ++-- docs/orleans/host/silo-lifecycle.md | 35 +-- .../implementation/cluster-management.md | 171 ++++++------ .../orleans/implementation/grain-directory.md | 40 +-- docs/orleans/implementation/index.md | 21 +- docs/orleans/implementation/load-balancing.md | 19 +- .../messaging-delivery-guarantees.md | 21 +- .../implementation/orleans-lifecycle.md | 23 +- docs/orleans/implementation/scheduler.md | 36 +-- .../azure-queue-streams.md | 29 +- docs/orleans/implementation/testing.md | 21 +- docs/orleans/migration-guide.md | 118 ++++---- docs/orleans/overview.md | 81 +++--- .../build-your-first-orleans-app.md | 16 +- .../deploy-scale-orleans-on-azure.md | 261 +++++++++--------- docs/orleans/resources/best-practices.md | 28 +- docs/orleans/resources/nuget-packages.md | 18 +- ...ns-architecture-principles-and-approach.md | 22 +- docs/orleans/streaming/index.md | 6 +- docs/orleans/streaming/stream-providers.md | 17 +- .../streaming/streams-programming-apis.md | 87 +++--- docs/orleans/streaming/streams-quick-start.md | 33 +-- docs/orleans/streaming/streams-why.md | 49 ++-- .../tutorials-and-samples/adventure.md | 25 +- .../custom-grain-storage.md | 97 +++---- docs/orleans/tutorials-and-samples/index.md | 142 +++++----- .../overview-helloworld.md | 46 ++- .../tutorials-and-samples/tutorial-1.md | 33 +-- 105 files changed, 2380 insertions(+), 2398 deletions(-) diff --git a/docs/orleans/benefits.md b/docs/orleans/benefits.md index 24af7c8d2e740..ba1a1676387df 100644 --- a/docs/orleans/benefits.md +++ b/docs/orleans/benefits.md @@ -1,75 +1,75 @@ --- title: Orleans benefits description: Learn the many benefits of .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: overview --- # Orleans benefits The main benefits of Orleans are: -- **Developer productivity**, even for non-expert programmers. -- **Transparent scalability by default** with no special effort from the programmer. +- **Developer productivity**: Even for non-expert programmers. +- **Transparent scalability by default**: Requires no special effort from the developer. ## Developer productivity -The Orleans programming model raises the productivity of both expert and non-expert programmers by providing the following key abstractions, guarantees, and system services. +The Orleans programming model raises productivity, regardless of expertise level, by providing the following key abstractions, guarantees, and system services. ### Familiar object-oriented programming (OOP) paradigm -Grains are .NET classes that implement declared .NET grain interfaces with asynchronous methods. Grains appear to the programmer as remote objects whose methods can be directly invoked. This provides the programmer the familiar OOP paradigm by turning method calls into messages, routing them to the right endpoints, invoking the target grain's methods, and dealing with failures and corner cases transparently. +Grains are .NET classes that implement declared .NET grain interfaces with asynchronous methods. Grains appear as remote objects whose methods can be directly invoked. This provides the familiar OOP paradigm by turning method calls into messages, routing them to the right endpoints, invoking the target grain's methods, and transparently handling failures and corner cases. ### Single-threaded execution of grains -The runtime guarantees that a grain never executes on more than one thread at a time. Combined with the isolation from other grains, the programmer never faces concurrency at the grain level, and never needs to use locks or other synchronization mechanisms to control access to shared data. This feature alone makes the development of distributed applications tractable for non-expert programmers. +The runtime guarantees a grain never executes on more than one thread at a time. Combined with isolation from other grains, developers never face concurrency at the grain level and never need locks or other synchronization mechanisms to control access to shared data. This feature alone makes developing distributed applications tractable, even for non-expert programmers. ### Transparent activation -The runtime activates a grain only when there is a message for it to process. This cleanly separates the notion of creating a reference to a grain, which is visible to and controlled by application code, and physical activation of the grain in memory, which is transparent to the application. This is similar to virtual memory in that it decides when to "page out" (deactivate) or "page in" (activate) a grain; The application has uninterrupted access to the full "memory space" of logically created grains, whether or not they are in the physical memory at any particular point in time. +The runtime activates a grain only when there's a message for it to process. This cleanly separates creating a grain reference (controlled by application code) and physical activation of the grain in memory (transparent to the application). This is similar to [virtual memory](https://wikipedia.org/wiki/Virtual_memory) where the OS decides when to bring pages into memory and when to evict pages from memory. Similarly, in Orleans, the runtime decides when to activate a grain (bringing it into memory) and when to deactivate a grain (evicting it from memory). The application has uninterrupted access to the full "memory space" of logically created grains, whether they are in physical memory at any given time. -Transparent activation enables dynamic, adaptive load balancing via placement and migration of grains across the pool of hardware resources. This feature is a significant improvement on the traditional actor model, in which actor lifetime is application-managed. +Transparent activation enables dynamic, adaptive load balancing via placement and migration of grains across the pool of hardware resources. This feature significantly improves on the traditional actor model, where actor lifetime is application-managed. ### Location transparency -A grain reference (proxy object) that the programmer uses to invoke the grain's methods or pass to other components contains only the logical identity of the grain. The translation of the grain's logical identity to its physical location and the corresponding routing of messages is done transparently by the Orleans runtime. +A grain reference (proxy object) that's used to invoke a grain's methods or pass to other components contains only the grain's logical identity. The Orleans runtime transparently handles translating the grain's logical identity to its physical location and routing messages accordingly. -Application code communicates with grains while remaining oblivious to their physical location, which may change over time due to failures or resource management or because a grain is deactivated at the time it is called. +Application code communicates with grains without knowing their physical location. This location might change over time due to failures, resource management, or because a grain is deactivated when called. ### Transparent integration with a persistent store -Orleans allows for declarative mapping of a grain's in-memory state to a persistent store. It synchronizes updates, transparently guaranteeing that callers receive results only after the persistent state has been successfully updated. Extending and/or customizing the set of existing persistent storage providers available is straightforward. +Orleans allows declaratively mapping a grain's in-memory state to a persistent store. It synchronizes updates, transparently guaranteeing callers receive results only after the persistent state successfully updates. Extending and/or customizing the set of existing persistent storage providers is straightforward. ### Automatic propagation of errors -The runtime automatically propagates unhandled errors up the call chain with the semantics of asynchronous and distributed try/catch. As a result, errors do not get lost within an application. This allows the programmer to put error handling logic at the appropriate places, without the tedious work of manually propagating errors at each level. +The runtime automatically propagates unhandled errors up the call chain with the semantics of asynchronous and distributed try/catch. As a result, errors don't get lost within an application. This allows placing error handling logic in appropriate places without the tedious work of manually propagating errors at each level. ## Transparent scalability by default -The Orleans programming model is designed to guide the programmer down a path of likely success in scaling an application or service through several orders of magnitude. This is done by incorporating proven best practices and patterns and by providing an efficient implementation of the lower-level system functionality. +The Orleans programming model guides developers toward successfully scaling applications or services through several orders of magnitude. It achieves this by incorporating proven best practices and patterns and providing an efficient implementation of lower-level system functionality. -Here are some key factors that enable scalability and performance: +Here are some key factors enabling scalability and performance: -### Implicit fine-grain partitioning of application state +### Implicit fine-grained partitioning of application state -By using grains as directly addressable entities, the programmer implicitly breaks down the overall state of their application. -While the Orleans programming model does not prescribe how big or small a grain should be, in most cases it makes sense to have a relatively large number of grains – millions or more – with each representing a natural entity of the application, such as a user account or a purchase order. +Using grains as directly addressable entities implicitly breaks down the application's overall state. While the Orleans programming model doesn't prescribe grain size, in most cases, having a relatively large number of grains (millions or more) makes sense, with each representing a natural application entity, such as a user account or purchase order. -With grains being individually addressable and their physical location abstracted away by the runtime, Orleans has enormous flexibility in balancing load and dealing with hot spots transparently and generically without any thought from the application developer. +With grains being individually addressable and their physical location abstracted by the runtime, Orleans has enormous flexibility in balancing load and dealing with hot spots transparently and generically, without requiring thought from the application developer. ### Adaptive resource management -Grains do not assume the locality of other grains as they interact with them. Because of this location transparency, the runtime can manage and adjust the allocation of available hardware resources dynamically. The runtime does this by making fine-grained decisions on placement and migration of grains across the compute cluster in reaction to load and communication patterns—without failing incoming requests. By creating multiple replicas of a particular grain, the runtime can increase the throughput of the grain without making any changes to the application code. +Grains don't assume the locality of other grains when interacting. Because of this location transparency, the runtime can dynamically manage and adjust the allocation of available hardware resources. The runtime achieves this by making fine-grained decisions on placing and migrating grains across the compute cluster in reaction to load and communication patterns — without failing incoming requests. By creating multiple replicas of a particular grain, the runtime can increase its throughput without changing application code. ### Multiplexed communication -Grains in Orleans have logical endpoints, and messaging among them is multiplexed across a fixed set of all-to-all physical connections (TCP sockets). This allows the runtime to host millions of addressable entities with low OS overhead per grain. In addition, activation and deactivation of a grain doesn't incur the cost of registering/unregistering a physical endpoint, such as a TCP port or HTTP URL or even closing a TCP connection. +Grains in Orleans have logical endpoints, and messaging among them is multiplexed across a fixed set of all-to-all physical connections (TCP sockets). This allows the runtime to host millions of addressable entities with low OS overhead per grain. Additionally, activating and deactivating a grain doesn't incur the cost of registering/unregistering a physical endpoint (like a TCP port or HTTP URL) or even closing a TCP connection. ### Efficient scheduling -The runtime schedules execution of a large number of single-threaded grains using the .NET Thread Pool, which is highly optimized for performance. With grain code written in the non-blocking, continuation-based style (a requirement of the Orleans programming model), application code runs in a very efficient "cooperative" multi-threaded manner with no contention. This allows the system to reach high throughput and run at very high CPU utilization (up to 90%+) with great stability. +The runtime schedules the execution of many single-threaded grains using the .NET Thread Pool, which is highly optimized for performance. When grain code is written in the non-blocking, continuation-based style (a requirement of the Orleans programming model), the application code runs very efficiently in a "cooperative" multi-threaded manner with no contention. This allows the system to achieve high throughput and run at very high CPU utilization (up to 90%+) with great stability. -The fact that growth in the number of grains in the system and an increase in the load does not lead to additional threads or other OS primitives helps scalability of individual nodes and the whole system. +The fact that growth in the number of grains and increased load doesn't lead to additional threads or other OS primitives helps the scalability of individual nodes and the whole system. ### Explicit asynchrony -The Orleans programming model makes the asynchronous nature of a distributed application explicit and guides programmers to write non-blocking asynchronous code. Combined with asynchronous messaging and efficient scheduling, this enables a large degree of distributed parallelism and overall throughput without the explicit use of multi-threading. +The Orleans programming model makes the asynchronous nature of distributed applications explicit and guides developers to write non-blocking asynchronous code. Combined with asynchronous messaging and efficient scheduling, this enables a large degree of distributed parallelism and overall throughput without requiring explicit multi-threading. diff --git a/docs/orleans/deployment/consul-deployment.md b/docs/orleans/deployment/consul-deployment.md index 5baa9e2c6fe9d..1df3c625badcc 100644 --- a/docs/orleans/deployment/consul-deployment.md +++ b/docs/orleans/deployment/consul-deployment.md @@ -1,28 +1,30 @@ --- title: Use Consul as a membership provider description: Learn how to use Consul as a membership provider in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: how-to +ms.custom: devops --- # Use Consul as a membership provider -[Consul](https://www.consul.io) is a distributed, highly available, and data center-aware service discovery platform that includes simple service registration, health checking, failure detection, and key-value storage. It's built on the premise that every node in the data center is running a Consul agent that is either acting as a server or client. Each agent communicates via a scalable gossip protocol. +[Consul](https://www.consul.io) is a distributed, highly available, and data center-aware service discovery platform including simple service registration, health checking, failure detection, and key-value storage. It's built on the premise that every node in the data center runs a Consul agent acting as either a server or client. Each agent communicates via a scalable gossip protocol. -There's a detailed overview of Consul including comparisons with similar solutions at [What is Consul?](https://www.consul.io/intro/index.html). +A detailed overview of Consul, including comparisons with similar solutions, is available at [What is Consul?](https://developer.hashicorp.com/consul). -Consul is written in [Go](https://go.dev) and is [open source](https://github.com/hashicorp/consul); compiled downloads are available for [macOS X, FreeBSD, Linux, Solaris, and Windows](https://www.consul.io/downloads.html). +Consul is written in [Go](https://go.dev) and is [open source](https://github.com/hashicorp/consul). Compiled downloads are available for [macOS X, FreeBSD, Linux, Solaris, and Windows](https://developer.hashicorp.com/consul/install). ## Why choose Consul? -As an [Orleans Membership Provider](../implementation/cluster-management.md), Consul is a good choice when you need to deliver an on-premises solution that doesn't require your potential customers to have existing infrastructure and a cooperative IT provider. Consul is a lightweight single executable, has no dependencies, and as such can easily be built into your middleware solution. When Consul is your solution for discovering, checking, and maintaining your microservices, it makes sense to fully integrate with Orleans membership for simplicity and ease of operation. There also exists a membership table in Consul (also known as "Orleans Custom System Store"), which fully integrates with Orleans's [Cluster Management](../implementation/cluster-management.md). +As an [Orleans Membership Provider](../implementation/cluster-management.md), Consul is a good choice for delivering on-premises solutions that don't require customers to have existing infrastructure or a cooperative IT provider. Consul is a lightweight single executable with no dependencies, making it easy to build into a middleware solution. When using Consul for discovering, checking, and maintaining microservices, fully integrating with Orleans membership offers simplicity and ease of operation. Consul also provides a membership table (also known as "Orleans Custom System Store") that fully integrates with Orleans's [Cluster Management](../implementation/cluster-management.md). ## Set up Consul -There's extensive documentation available on [Consul.io](https://www.consul.io) about setting up a stable Consul cluster, and it doesn't make sense to repeat that here. However, for your convenience, we include this guide so you can quickly get Orleans running with a standalone Consul agent. +Extensive documentation on setting up a stable Consul cluster is available in the [Consul documentation](https://developer.hashicorp.com/consul), so that information won't be repeated here. However, for convenience, this guide shows how to quickly get Orleans running with a standalone Consul agent. 1. Create a folder to install Consul into (for example _C:\Consul_). 1. Create a subfolder: _C:\Consul\Data_ (Consul doesn't create this directory if it doesn't exist). -1. [Download](https://www.consul.io/downloads.html) and unzip _Consul.exe_ into _C:\Consul_. +1. [Download](https://developer.hashicorp.com/consul/install) and unzip _Consul.exe_ into _C:\Consul_. 1. Open a command prompt at _C:\Consul_ and run the following command: ```powershell @@ -31,46 +33,46 @@ There's extensive documentation available on [Consul.io](https://www.consul.io) In the preceding command: - - `agent`: Instructs Consul to run the agent process that hosts the services. Without this switch, the Consul process attempts to use RPC to configure a running agent. - - `-server`: Defines the agent as a server and not a client (A Consul _client_ is an agent that hosts all the services and data, but doesn't have voting rights to decide, and can't become, the cluster leader. - - `-bootstrap`: The first (and only the first!) node in a cluster must be bootstrapped so that it assumes the cluster leadership. - - `-data-dir [path]`: Specifies the path where all Consul data is stored, including the cluster membership table. - - `-client='0.0.0.0'`: Informs Consul which IP to open the service on. + - `agent`: Instructs Consul to run the agent process hosting the services. Without this switch, the Consul process attempts to use RPC to configure a running agent. + - `-server`: Defines the agent as a server, not a client. (A Consul _client_ is an agent hosting services and data but lacks voting rights and cannot become the cluster leader). + - `-bootstrap`: The first (and only the first!) node in a cluster must be bootstrapped to assume cluster leadership. + - `-data-dir [path]`: Specifies the path where all Consul data, including the cluster membership table, is stored. + - `-client='0.0.0.0'`: Informs Consul which IP address to open the service on. - There are many other parameters, and the option to use a JSON configuration file. For a full listing of the options, see the Consul documentation. + Many other parameters exist, including the option to use a JSON configuration file. See the Consul documentation for a full listing. -1. Verify that Consul is running and ready to accept membership requests from Orleans by opening the services endpoint in your browser at `http://localhost:8500/v1/catalog/services`. When functioning correctly, the browser displays the following JSON: +1. Verify Consul is running and ready to accept membership requests from Orleans by opening the services endpoint in your browser at `http://localhost:8500/v1/catalog/services`. When functioning correctly, the browser displays the following JSON: - ```json - { - "consul": [] - } - ``` + ```json + { + "consul": [] + } + ``` ## Configure Orleans -To configure Orleans to use Consul as a membership provider, your silo project will need to reference the [Microsoft.Orleans.Clustering.Consul](https://www.nuget.org/packages/Microsoft.Orleans.Clustering.Consul) NuGet package. Once you've done that, you can configure the membership provider in your silo's _Program.cs_ file as follows: +To configure Orleans to use Consul as a membership provider, the silo project needs to reference the [Microsoft.Orleans.Clustering.Consul](https://www.nuget.org/packages/Microsoft.Orleans.Clustering.Consul) NuGet package. After adding the reference, configure the membership provider in the silo's _Program.cs_ file as follows: :::code source="snippets/consul/Silo/Program.cs"::: The preceding code: -- Creates a with defaults from the . -- Chains a call to which configures the Orleans silo. -- Given the calls . +- Creates an with defaults from . +- Chains a call to to configure the Orleans silo. +- Given the , calls . - Configures the cluster membership provider to use Consul, given the Consul `address`. To configure the client, reference the same NuGet package and call the extension method. ## Client SDK -If you're interested in using Consul for your service discovery, there are [Client SDKs](https://www.consul.io/downloads_tools.html) for most popular languages. +If interested in using Consul for service discovery, [Client SDKs](https://developer.hashicorp.com/consul/docs/integrate/consul-tools) are available for most popular languages. -### Implementation detail +### Implementation details -The Membership Table Provider makes use of [Consul's Key/Value store](https://www.consul.io/intro/getting-started/kv.html) functionality with Check-And-Set (CAS) operations. When each Silo starts, it registers two key-value entries, one that contains the Silo details and one that holds the last time the Silo reported it was alive. The latter refers to diagnostics "I'm alive" entries and not to failure detection heartbeats, which are sent directly between the silos and aren't written into the table. All writes to the table are performed with CAS to provide concurrency control, as necessitated by Orleans's [Cluster Management Protocol](../implementation/cluster-management.md). +The Membership Table Provider uses [Consul's Key/Value store](https://developer.hashicorp.com/consul/api-docs/kv) functionality with Check-And-Set (CAS) operations. When each Silo starts, it registers two key-value entries: one containing Silo details and one holding the last time the Silo reported it was alive. The latter refers to diagnostic "I'm alive" entries, not failure detection heartbeats, which are sent directly between silos and aren't written to the table. All writes to the table use CAS to provide concurrency control, as required by Orleans's [Cluster Management Protocol](../implementation/cluster-management.md). -Once the Silo is running, you can view these entries in your web browser at `http://localhost:8500/v1/kv/?keys&pretty`, which displays something like: +Once the Silo runs, view these entries in a web browser at `http://localhost:8500/v1/kv/?keys&pretty`. The output looks similar to this: ```json [ @@ -80,7 +82,7 @@ Once the Silo is running, you can view these entries in your web browser at `htt ] ``` -All of the keys are prefixed with `orleans`, which is hard coded in the provider and is intended to avoid keyspace collision with other users of Consul. You can use any of these keys to retrieve additional information about Each of these keys can be read by appending their key name (without quotes) to the Consul KV root at `http://localhost:8500/v1/kv/`. Doing so presents you with the following JSON: +All keys are prefixed with `orleans`. This prefix is hardcoded in the provider and intended to avoid keyspace collisions with other Consul users. Retrieve additional information for each key by appending its name (without quotes) to the Consul KV root at `http://localhost:8500/v1/kv/`. Doing so presents the following JSON: ```json [ @@ -95,7 +97,7 @@ All of the keys are prefixed with `orleans`, which is hard coded in the provider ] ``` -Decoding the Base64 UTF-8 encoded string `Value` gives you the actual Orleans membership data: +Decoding the Base64 UTF-8 encoded string `Value` provides the actual Orleans membership data: **`http://localhost:8500/v1/KV/orleans/default/[SiloAddress]`** @@ -116,19 +118,19 @@ Decoding the Base64 UTF-8 encoded string `Value` gives you the actual Orleans me "2023-05-15T14:27:01.1832828Z" ``` -When the clients connect, they read the KVs for all silos in the cluster in one HTTP GET by using the URI `http://192.168.1.26:8500/v1/KV/orleans/default/?recurse`. +When clients connect, they read the KVs for all silos in the cluster in one HTTP GET request using the URI `http://localhost:8500/v1/KV/orleans/default/?recurse`. ## Limitations -There are a few limitations to be aware of when using Consul as a membership provider. +Be aware of a few limitations when using Consul as a membership provider. -### Orleans extended membership protocol (table version & ETag) +### Orleans extended membership protocol (table version and ETag) -Consul KV currently doesn't support atomic updates. Therefore, the Orleans Consul Membership Provider only implements the Orleans basic membership protocol, as described in [Cluster management in Orleans](../implementation/cluster-management.md), and doesn't support the Extended Membership Protocol. This Extended protocol was introduced as an extra, but not essential, silo connectivity validation and as a foundation to functionality that hasn't yet been implemented. +Consul KV currently doesn't support atomic updates. Therefore, the Orleans Consul Membership Provider only implements the Orleans basic membership protocol, as described in [Cluster management in Orleans](../implementation/cluster-management.md). It doesn't support the Extended Membership Protocol. This Extended protocol was introduced as an extra, though not essential, silo connectivity validation and as a foundation for functionality not yet implemented. ### Multiple datacenters -The key-value pairs in Consul aren't currently replicated between Consul data centers. There's a [separate project](https://github.com/hashicorp/consul-replicate) to address this replication effort, but it hasn't yet been proven to support Orleans. +Key-value pairs in Consul aren't currently replicated between Consul data centers. A [separate project](https://github.com/hashicorp/consul-replicate) exists to address this replication effort, but it hasn't yet been proven to support Orleans. ### When running on Windows @@ -138,11 +140,11 @@ When Consul starts on Windows, it logs the following message: ==> WARNING: Windows is not recommended as a Consul server. Do not use in production. ``` -This warning message is displayed due to a lack of focus on testing when running in a Windows environment and not because of any actual known issues. Read the [discussion](https://groups.google.com/forum/#!topic/consul-tool/DvXYgZtUZyU) before deciding if Consul is the right choice for you. +This warning message appears due to a lack of focus on testing when running in a Windows environment, not because of any actual known issues. Read the [discussion](https://groups.google.com/forum/#!topic/consul-tool/DvXYgZtUZyU) before deciding if Consul is the right choice. ## Potential future enhancements -1. Prove that the Consul KV replication project can support an Orleans cluster in a WAN environment between multiple Consul data centers. +1. Prove the Consul KV replication project can support an Orleans cluster in a WAN environment between multiple Consul data centers. 1. Implement the Reminder Table in Consul. 1. Implement the Extended Membership Protocol. -The team behind Consul does plan on implementing atomic operations, once this functionality is available it's possible to remove the limitations in the provider. +The team behind Consul plans to implement atomic operations. Once this functionality is available, removing the limitations in the provider might be possible. diff --git a/docs/orleans/deployment/deploy-to-azure-app-service.md b/docs/orleans/deployment/deploy-to-azure-app-service.md index afd3e53a65410..5bb2070062798 100644 --- a/docs/orleans/deployment/deploy-to-azure-app-service.md +++ b/docs/orleans/deployment/deploy-to-azure-app-service.md @@ -1,22 +1,22 @@ --- title: Deploy Orleans to Azure App Service description: Learn how to deploy an Orleans shopping cart app to Azure App Service. -ms.date: 10/01/2024 +ms.date: 05/23/2025 ms.topic: tutorial ms.custom: devx-track-bicep --- # Deploy Orleans to Azure App Service -In this tutorial, you learn how to deploy an Orleans shopping cart app to Azure App Service. The tutorial walks you through a sample application that supports the following features: +In this tutorial, learn how to deploy an Orleans shopping cart app to Azure App Service. This guide walks through a sample application supporting the following features: -- **Shopping cart**: A simple shopping cart application that uses Orleans for its cross-platform framework support, and its scalable distributed applications capabilities. +- **Shopping cart**: A simple shopping cart application using Orleans for its cross-platform framework support and scalable distributed application capabilities. - **Inventory management**: Edit and/or create product inventory. - - **Shop inventory**: Explore purchasable products and add them to your cart. - - **Cart**: View a summary of all the items in your cart, and manage these items; either removing or changing the quantity of each item. + - **Shop inventory**: Explore purchasable products and add them to the cart. + - **Cart**: View a summary of all items in the cart and manage these items by removing or changing the quantity of each item. -With an understanding of the app and its features, you will then learn how to deploy the app to Azure App Service using GitHub Actions, the .NET and Azure CLIs, and Azure Bicep. Additionally, you'll learn how to configure the virtual network for the app within Azure. +With an understanding of the app and its features, learn how to deploy the app to Azure App Service using GitHub Actions, the .NET and Azure CLIs, and Azure Bicep. Additionally, learn how to configure the virtual network for the app within Azure. In this tutorial, you learn how to: @@ -37,17 +37,17 @@ In this tutorial, you learn how to: ## Run the app locally -To run the app locally, fork the [Azure Samples: Orleans Cluster on Azure App Service](https://github.com/Azure-Samples/Orleans-Cluster-on-Azure-App-Service) repository and clone it to your local machine. Once cloned, open the solution in an IDE of your choice. If you're using Visual Studio, right-click the **Orleans.ShoppingCart.Silo** project and select **Set As Startup Project**, then run the app. Otherwise, you can run the app using the following .NET CLI command: +To run the app locally, fork the [Azure Samples: Orleans Cluster on Azure App Service](https://github.com/Azure-Samples/Orleans-Cluster-on-Azure-App-Service) repository and clone it to your local machine. Once cloned, open the solution in an IDE of your choice. If using Visual Studio, right-click the **Orleans.ShoppingCart.Silo** project, select **Set As Startup Project**, then run the app. Otherwise, run the app using the following .NET CLI command: ```dotnetcli dotnet run --project Silo\Orleans.ShoppingCart.Silo.csproj ``` -For more information, see [dotnet run](../../core/tools/dotnet-run.md). With the app running, you can navigate around and you're free to test out its capabilities. All of the app's functionality when running locally relies on in-memory persistence, local clustering, and it uses the [Bogus NuGet](https://www.nuget.org/packages/Bogus) package to generate fake products. Stop the app either by selecting the **Stop Debugging** option in Visual Studio or by pressing Ctrl+C in the .NET CLI. +For more information, see [dotnet run](../../core/tools/dotnet-run.md). With the app running, navigate around and test its capabilities. All app functionality when running locally relies on in-memory persistence and local clustering. It also uses the [Bogus NuGet](https://www.nuget.org/packages/Bogus) package to generate fake products. Stop the app either by selecting the **Stop Debugging** option in Visual Studio or by pressing Ctrl+C in the .NET CLI. ## Inside the shopping cart app -Orleans is a reliable and scalable framework for building distributed applications. For this tutorial, you will deploy a simple shopping cart app built using Orleans to Azure App Service. The app exposes the ability to manage inventory, add and remove items in a cart, and shop available products. The client is built using Blazor with a server hosting model. The app is architected as follows: +Orleans is a reliable and scalable framework for building distributed applications. For this tutorial, deploy a simple shopping cart app built using Orleans to Azure App Service. The app exposes the ability to manage inventory, add and remove items in a cart, and shop available products. The client is built using Blazor with a server hosting model. The app is architected as follows: :::image type="content" source="media/shopping-cart-app-arch.png" lightbox="media/shopping-cart-app-arch.png" alt-text="Orleans: Shopping Cart sample app architecture."::: @@ -55,72 +55,72 @@ The preceding diagram shows that the client is the server-side Blazor app. It's - `InventoryService`: Consumes the `IInventoryGrain` where inventory is partitioned by product category. - `ProductService`: Consumes the `IProductGrain` where a single product is tethered to a single grain instance by `Id`. -- `ShoppingCartService`: Consumes the `IShoppingCartGrain` where a single user only has a single shopping cart instance regardless of consuming clients. +- `ShoppingCartService`: Consumes the `IShoppingCartGrain` where a single user only has a single shopping cart instance regardless of consuming clients. The solution contains three projects: -- `Orleans.ShoppingCart.Abstractions`: A class library that defines the models and the interfaces for the app. -- `Orleans.ShoppingCart.Grains`: A class library that defines the grains that implement the app's business logic. -- `Orleans.ShoppingCart.Silos`: A server-side Blazor app that hosts the Orleans silo. +- `Orleans.ShoppingCart.Abstractions`: A class library defining the models and interfaces for the app. +- `Orleans.ShoppingCart.Grains`: A class library defining the grains that implement the app's business logic. +- `Orleans.ShoppingCart.Silos`: A server-side Blazor app hosting the Orleans silo. ### The client user experience -The shopping cart client app has several pages, each of which represents a different user experience. The app's UI is built using the [MudBlazor NuGet](https://www.nuget.org/packages/MudBlazor) package. +The shopping cart client app has several pages, each representing a different user experience. The app's UI is built using the [MudBlazor NuGet](https://www.nuget.org/packages/MudBlazor) package. **Home page** -A few simple phrases for the user to understand the app's purpose, and add context to each navigation menu item. +A few simple phrases help understand the app's purpose and add context to each navigation menu item. :::image type="content" source="media/home-page.png" lightbox="media/home-page.png" alt-text="Orleans: Shopping Cart sample app, home page."::: **Shop inventory page** -A page that displays all of the products that are available for purchase. Items can be added to the cart from this page. +A page displaying all products available for purchase. Items can be added to the cart from this page. :::image type="content" source="media/shop-inventory-page.png" lightbox="media/shop-inventory-page.png" alt-text="Orleans: Shopping Cart sample app, shop inventory page."::: **Empty cart page** -When you haven't added anything to your cart, the page renders a message that indicates that you have no items in your cart. +If nothing has been added to the cart, the page renders a message indicating no items are in the cart. :::image type="content" source="media/empty-shopping-cart-page.png" lightbox="media/empty-shopping-cart-page.png" alt-text="Orleans: Shopping Cart sample app, empty cart page."::: **Items added to the cart while on the shop inventory page** -When items are added to your cart while on the shop inventory page, the app displays a message that indicates the item was added to the cart. +When items are added to the cart while on the shop inventory page, the app displays a message indicating the item was added. :::image type="content" source="media/shop-inventory-items-added-page.png" lightbox="media/shop-inventory-items-added-page.png" alt-text="Orleans: Shopping Cart sample app, items added to cart while on shop inventory page."::: **Product management page** -A user can manage inventory from this page. Products can be added, edited, and removed from the inventory. +Manage inventory from this page. Products can be added, edited, and removed from the inventory. :::image type="content" source="media/product-management-page.png" lightbox="media/product-management-page.png" alt-text="Orleans: Shopping Cart sample app, product management page."::: **Product management page create new dialog** -When a user clicks the **Create new product** button, the app displays a dialog that allows the user to create a new product. +Clicking the **Create new product** button displays a dialog allowing creation of a new product. :::image type="content" source="media/product-management-page-new.png" lightbox="media/product-management-page-new.png" alt-text="Orleans: Shopping Cart sample app, product management page - create new product dialog."::: **Items in the cart page** -When items are in your cart, you can view them and change their quantity, and even remove them from the cart. The user is shown a summary of the items in the cart and the pretax total cost. +When items are in the cart, view them, change their quantity, and even remove them. A summary of the items in the cart and the pretax total cost is shown. :::image type="content" source="media/items-in-shopping-cart-page.png" lightbox="media/items-in-shopping-cart-page.png" alt-text="Orleans: Shopping Cart sample app, items in cart page."::: > [!IMPORTANT] -> When this app runs locally, in a development environment, the app will use localhost clustering, in-memory storage, and a local silo. It also seeds the inventory with fake data that is automatically generated using the [Bogus NuGet](https://www.nuget.org/packages/bogus) package. This is all intentional to demonstrate the functionality. +> When this app runs locally in a development environment, it uses localhost clustering, in-memory storage, and a local silo. It also seeds the inventory with fake data automatically generated using the [Bogus NuGet](https://www.nuget.org/packages/bogus) package. This is intentional to demonstrate functionality. ## Deployment overview -Orleans applications are designed to scale up and scale out efficiently. To accomplish this, instances of your application communicate directly with each other via TCP sockets and therefore Orleans requires network connectivity between silos. Azure App Service supports this requirement via [virtual network integration](/azure/app-service/overview-vnet-integration) and additional configuration instructing App Service to allocate private network ports for your app instances. +Orleans applications are designed to scale up and scale out efficiently. To accomplish this, application instances communicate directly via TCP sockets. Therefore, Orleans requires network connectivity between silos. Azure App Service supports this requirement via [virtual network integration](/azure/app-service/overview-vnet-integration) and additional configuration instructing App Service to allocate private network ports for app instances. -When deploying Orleans to Azure App Service, we need to take the following actions to ensure that hosts can communicate with eachother: +When deploying Orleans to Azure App Service, take the following actions to ensure hosts can communicate: - Enable virtual network integration by following the [Enable integration with an Azure virtual network](/azure/app-service/configure-vnet-integration-enable) guide. -- Configure your app with private ports using the Azure CLI as described in the [Configure private port count using Azure CLI](#configure-private-port-count-using-azure-cli) section. The Bicep template in the [Explore the Bicep templates](#explore-the-bicep-templates) section below shows how to configure the setting via Bicep. -- If deploying to Linux, ensure that your hosts are listening on all IP addresses as described in the [Configure host networking](#configure-host-networking) section. +- Configure the app with private ports using the Azure CLI as described in the [Configure private port count using Azure CLI](#configure-private-port-count-using-azure-cli) section. The Bicep template in the [Explore the Bicep templates](#explore-the-bicep-templates) section below shows how to configure this setting via Bicep. +- If deploying to Linux, ensure hosts listen on all IP addresses as described in the [Configure host networking](#configure-host-networking) section. ### Configure private port count using Azure CLI @@ -130,12 +130,12 @@ az webapp config set -g '' --subscription ' [!NOTE] > For a reliable production deployment, you'd want more than one silo in a cluster for fault tolerance and scale. @@ -168,7 +168,7 @@ A typical Orleans application consists of a cluster of server processes (silos) ### Prepare for Azure deployment -The app will need to be packaged for deployment. In the `Orleans.ShoppingCart.Silos` project we define a `Target` element that runs after the `Publish` step. This will zip the publish directory into a _silo.zip_ file: +Package the app for deployment. In the `Orleans.ShoppingCart.Silos` project, a `Target` element is defined that runs after the `Publish` step. This target zips the publish directory into a _silo.zip_ file: ```xml @@ -177,7 +177,7 @@ The app will need to be packaged for deployment. In the `Orleans.ShoppingCart.Si ``` -There are many ways to deploy a .NET app to Azure App Service. In this tutorial, you use GitHub Actions, Azure Bicep, and the .NET and Azure CLIs. Consider the _./github/workflows/deploy.yml_ file in the root of the GitHub repository: +There are many ways to deploy a .NET app to Azure App Service. In this tutorial, use GitHub Actions, Azure Bicep, and the .NET and Azure CLIs. Consider the _./github/workflows/deploy.yml_ file in the root of the GitHub repository: ```yml name: Deploy to Azure App Service @@ -236,15 +236,15 @@ jobs: --type zip --src-path silo.zip --debug ``` -The preceding GitHub workflow will: +The preceding GitHub workflow does the following: -- Publish the shopping cart app as a zip file, using the [dotnet publish](../../core/tools/dotnet-publish.md) command. -- Login to Azure using the credentials from the [Create a service principal](#create-a-service-principal) step. -- Evaluate the _main.bicep_ file and start a deployment group using [az deployment group create](/cli/azure/deployment/group#az-deployment-group-create). -- Deploy the _silo.zip_ file to Azure App Service using [az webapp deploy](/cli/azure/webapp#az-webapp-deploy). +- Publishes the shopping cart app as a zip file using the [dotnet publish](../../core/tools/dotnet-publish.md) command. +- Logs in to Azure using credentials from the [Create a service principal](#create-a-service-principal) step. +- Evaluates the _main.bicep_ file and starts a deployment group using [az deployment group create](/cli/azure/deployment/group#az-deployment-group-create). +- Deploys the _silo.zip_ file to Azure App Service using [az webapp deploy](/cli/azure/webapp#az-webapp-deploy). - An additional deployment to staging is also configured. -The workflow is triggered by a push to the _main_ branch. For more information, see [GitHub Actions and .NET](../../devops/github-actions-overview.md). +The workflow triggers on a push to the `main` branch. For more information, see [GitHub Actions and .NET](../../devops/github-actions-overview.md). > [!TIP] > If you encounter issues when running the workflow, you might need to verify that the service principal has all the required provider namespaces registered. The following provider namespaces are required: @@ -257,7 +257,7 @@ The workflow is triggered by a push to the _main_ branch. For more information, > > For more information, see [Resolve errors for resource provider registration](/azure/azure-resource-manager/troubleshooting/error-register-resource-provider?tabs=azure-cli). -Azure imposes naming restrictions and conventions for resources. You need to update the _deploy.yml_ file values for the following: +Azure imposes naming restrictions and conventions for resources. Update the values in the _deploy.yml_ file for the following environment variables: - `UNIQUE_APP_NAME` - `AZURE_RESOURCE_GROUP_NAME` @@ -269,12 +269,12 @@ For more information, see [Naming rules and restrictions for Azure resources](/a ## Explore the Bicep templates -When the `az deployment group create` command is run, it will evaluate the _main.bicep_ file. This file contains the Azure resources that you want to deploy. One way to think of this step is that it _provisions_ all of the resources for deployment. +When the `az deployment group create` command runs, it evaluates the _main.bicep_ file. This file contains the Azure resources to deploy. Think of this step as _provisioning_ all resources for deployment. > [!IMPORTANT] > If you're using Visual Studio Code, the bicep authoring experience is improved when using the [Bicep Extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-bicep). -There are many bicep files, each containing either resources or modules (collections of resources). The _main.bicep_ file is the entry point and is comprised primarily of `module` definitions: +There are many Bicep files, each containing either resources or modules (collections of resources). The _main.bicep_ file is the entry point and consists primarily of `module` definitions: ```bicep param appName string @@ -354,17 +354,17 @@ module siloModule 'app-service.bicep' = { } ``` -The preceding bicep file defines the following: +The preceding Bicep file defines the following: - Two parameters for the resource group name and the app name. -- The `storageModule` definition, which defines the storage account. -- The `logsModule` definition, which defines the Azure Log Analytics and Application Insights resources. -- The `vnet` resource, which defines the virtual network. -- The `siloModule` definition, which defines the Azure App Service. +- The `storageModule` definition, defining the storage account. +- The `logsModule` definition, defining the Azure Log Analytics and Application Insights resources. +- The `vnet` resource, defining the virtual network. +- The `siloModule` definition, defining the Azure App Service. -One very important `resource` is that of the Virtual Network. The `vnet` resource enables the Azure App Service to communicate with the Orleans cluster. +One very important `resource` is the Virtual Network. The `vnet` resource enables Azure App Service to communicate with the Orleans cluster. -Whenever a `module` is encountered in the bicep file, it is evaluated via another bicep file that contains the resource definitions. The first encountered module was the `storageModule`, which is defined in the _storage.bicep_ file: +Whenever a `module` is encountered in the Bicep file, it's evaluated via another Bicep file containing the resource definitions. The first module encountered is `storageModule`, defined in the _storage.bicep_ file: ```bicep param name string @@ -387,7 +387,7 @@ var endpointSuffix = 'EndpointSuffix=${environment().suffixes.storage}' output connectionString string = '${protocol};${accountBits};${endpointSuffix}' ``` -Bicep files accept parameters, which are declared using the `param` keyword. Likewise, they can also declare outputs using the `output` keyword. The storage `resource` relies on the `Microsoft.Storage/storageAccounts@2021-08-01` type and version. It will be provisioned in the resource group's location, as a `StorageV2` and `Standard_LRS` SKU. The storage bicep defines its connection string as an `output`. This `connectionString` is later used by the silo bicep to connect to the storage account. +Bicep files accept parameters, declared using the `param` keyword. Likewise, they can declare outputs using the `output` keyword. The storage `resource` relies on the `Microsoft.Storage/storageAccounts@2021-08-01` type and version. It's provisioned in the resource group's location as a `StorageV2` and `Standard_LRS` SKU. The storage Bicep file defines its connection string as an `output`. This `connectionString` is later used by the silo Bicep file to connect to the storage account. Next, the _logs-and-insights.bicep_ file defines the Azure Log Analytics and Application Insights resources: @@ -424,7 +424,7 @@ output appInsightsInstrumentationKey string = appInsights.properties.Instrumenta output appInsightsConnectionString string = appInsights.properties.ConnectionString ``` -This bicep file defines the Azure Log Analytics and Application Insights resources. The `appInsights` resource is a `web` type, and the `logs` resource is a `PerGB2018` type. Both the `appInsights` resource and the `logs` resource are provisioned in the resource group's location. The `appInsights` resource is linked to the `logs` resource via the `WorkspaceResourceId` property. There are two outputs defined in this bicep, used later by the App Service `module`. +This Bicep file defines the Azure Log Analytics and Application Insights resources. The `appInsights` resource is a `web` type, and the `logs` resource is a `PerGB2018` type. Both `appInsights` and `logs` resources are provisioned in the resource group's location. The `appInsights` resource links to the `logs` resource via the `WorkspaceResourceId` property. This Bicep file defines two outputs used later by the App Service `module`. Finally, the _app-service.bicep_ file defines the Azure App Service resource: @@ -535,24 +535,24 @@ resource appServiceConfig 'Microsoft.Web/sites/config@2021-03-01' = { } ``` -This bicep file configures the Azure App Service as a .NET 8 application. Both the `appServicePlan` resource and the `appService` resource are provisioned in the resource group's location. The `appService` resource is configured to use the `S1` SKU, with a capacity of `1`. Additionally, the resource is configured to use the `vnetSubnetId` subnet and to use HTTPS. It also configures the `appInsightsInstrumentationKey` instrumentation key, the `appInsightsConnectionString` connection string, and the `storageConnectionString` connection string. These are used by the shopping cart app. +This Bicep file configures Azure App Service as a .NET 8 application. Both `appServicePlan` and `appService` resources are provisioned in the resource group's location. The `appService` resource is configured to use the `S1` SKU with a capacity of `1`. Additionally, the resource is configured to use the `vnetSubnetId` subnet and HTTPS. It also configures the `appInsightsInstrumentationKey` instrumentation key, the `appInsightsConnectionString` connection string, and the `storageConnectionString` connection string. The shopping cart app uses these values. -The aforementioned Visual Studio Code extension for Bicep includes a visualizer. All of these bicep files are visualized as follows: +The aforementioned Visual Studio Code extension for Bicep includes a visualizer. All these Bicep files are visualized as follows: :::image type="content" source="media/shopping-cart-flexing.png" alt-text="Orleans: Shopping cart sample app bicep provisioning visualizer rendering." lightbox="media/shopping-cart-flexing.png"::: ### Staging environments -The deployment infrastructure can deploy to staging environments, which are short-lived, test-centric, and immutable throwaway environments. These environments are very helpful for testing deployments before promoting them to production. +The deployment infrastructure can deploy to staging environments. These are short-lived, test-centric, immutable throwaway environments very helpful for testing deployments before promoting them to production. > [!NOTE] -> If your App Service is running on Windows, each App Service must be on its own separate App Service Plan. Alternatively, to avoid such configuration, you could instead use App Service on Linux, and this problem would be resolved. +> If the App Service runs on Windows, each App Service must be on its own separate App Service Plan. Alternatively, to avoid such configuration, use App Service on Linux instead, and this problem is resolved. ## Summary -As you update the source code and `push` changes to the `main` branch of the repository, the _deploy.yml_ workflow will run. It will provide the resources defined in the bicep files and deploy the application. The application can be expanded upon to include new features, such as authentication, or to support multiple instances of the application. The primary objective of this workflow is to demonstrate the ability to provision and deploy resources in a single step. +As source code is updated and changes are `push`ed to the `main` branch of the repository, the _deploy.yml_ workflow runs. It provisions the resources defined in the Bicep files and deploys the application. The application can be expanded to include new features, such as authentication, or to support multiple instances. The primary objective of this workflow is demonstrating the ability to provision and deploy resources in a single step. -In addition to the visualizer from the bicep extension, the Azure portal resource group page would look similar to the following example after provisioning and deploying the application: +In addition to the visualizer from the Bicep extension, the Azure portal resource group page looks similar to the following example after provisioning and deploying the application: :::image type="content" source="media/shopping-cart-resources.png" alt-text="Azure Portal: Orleans shopping cart sample app resources." lightbox="media/shopping-cart-resources.png"::: diff --git a/docs/orleans/deployment/deploy-to-azure-container-apps.md b/docs/orleans/deployment/deploy-to-azure-container-apps.md index 9d79cbf44a073..037f66a72b28f 100644 --- a/docs/orleans/deployment/deploy-to-azure-container-apps.md +++ b/docs/orleans/deployment/deploy-to-azure-container-apps.md @@ -1,16 +1,16 @@ --- title: Deploy Orleans to Azure Container Apps description: Learn how to deploy an updated Orleans shopping cart app to Azure Container Apps. -ms.date: 07/03/2024 +ms.date: 05/23/2025 ms.topic: tutorial ms.custom: devx-track-bicep --- # Deploy Orleans to Azure Container Apps -In this tutorial, you'll learn how to deploy an example Orleans shopping cart application to Azure Container Apps. This tutorial expands the functionality of the [sample Orleans shopping cart app](https://github.com/Azure-Samples/Orleans-Cluster-on-Azure-App-Service), introduced in [Deploy Orleans to Azure App Service](deploy-to-azure-app-service.md). The sample app adds Azure Active Directory (AAD) business-to-consumer (B2C) authentication and deploys to Azure Container Apps. +In this tutorial, learn how to deploy an example Orleans shopping cart application to Azure Container Apps. This tutorial expands the functionality of the [sample Orleans shopping cart app](https://github.com/Azure-Samples/Orleans-Cluster-on-Azure-App-Service), introduced in [Deploy Orleans to Azure App Service](deploy-to-azure-app-service.md). The sample app adds Azure Active Directory (AAD) business-to-consumer (B2C) authentication and deploys to Azure Container Apps. -You'll learn how to deploy using GitHub Actions, the .NET and Azure CLIs, and Azure Bicep. Additionally, you'll learn how to configure the Container App's HTTP ingress. +Learn how to deploy using GitHub Actions, the .NET and Azure CLIs, and Azure Bicep. Additionally, learn how to configure the Container App's HTTP ingress. In this tutorial, you learn how to: @@ -31,30 +31,30 @@ In this tutorial, you learn how to: ## Run the app locally -To run the app locally, fork the [Azure Samples: Orleans shopping cart on Azure Container Apps](https://github.com/Azure-Samples/orleans-blazor-server-shopping-cart-on-container-apps) repository and clone it to your local machine. Once cloned, open the solution in an IDE of your choice. If you're using Visual Studio, right-click the **Orleans.ShoppingCart.Silo** project and select **Set As Startup Project**, then run the app. Otherwise, you can run the app using the following .NET CLI command: +To run the app locally, fork the [Azure Samples: Orleans shopping cart on Azure Container Apps](https://github.com/Azure-Samples/orleans-blazor-server-shopping-cart-on-container-apps) repository and clone it to your local machine. Once cloned, open the solution in an IDE of your choice. If using Visual Studio, right-click the **Orleans.ShoppingCart.Silo** project, select **Set As Startup Project**, then run the app. Otherwise, run the app using the following .NET CLI command: ```dotnetcli dotnet run --project Silo\Orleans.ShoppingCart.Silo.csproj ``` -For more information, see [dotnet run](../../core/tools/dotnet-run.md). With the app running, you're presented with a landing page that discusses the app's functionality. In the upper-right corner, you'll see a sign-in button. You can sign up for an account, or sign in if you already have an account. Once signed in, you can navigate around and you're free to test out its capabilities. All of the app's functionality when running locally relies on in-memory persistence, local clustering, and it uses the [Bogus NuGet](https://www.nuget.org/packages/Bogus) package to generate fake products. Stop the app either by selecting the **Stop Debugging** option in Visual Studio or by pressing Ctrl+C in the .NET CLI. +For more information, see [dotnet run](../../core/tools/dotnet-run.md). With the app running, a landing page discusses the app's functionality. In the upper-right corner, a sign-in button is visible. Sign up for an account or sign in if one already exists. Once signed in, navigate around and test its capabilities. All app functionality when running locally relies on in-memory persistence and local clustering. It also uses the [Bogus NuGet](https://www.nuget.org/packages/Bogus) package to generate fake products. Stop the app either by selecting the **Stop Debugging** option in Visual Studio or by pressing Ctrl+C in the .NET CLI. ### AAD B2C -While teaching the concepts of authentication are beyond the scope of this tutorial, you can learn how to [Create an Azure Active Directory B2C tenant](/azure/active-directory-b2c/tutorial-create-tenant), and then you can [Register a web app](/azure/active-directory-b2c/tutorial-register-applications) to consume it. In the case of this shopping cart example app, the resulting deployed Container Apps' URL will need to be registered in the B2C tenant. For more information, see [ASP.NET Core Blazor authentication and authorization](/aspnet/core/blazor/security). +While teaching authentication concepts is beyond the scope of this tutorial, learn how to [Create an Azure Active Directory B2C tenant](/azure/active-directory-b2c/tutorial-create-tenant), and then [Register a web app](/azure/active-directory-b2c/tutorial-register-applications) to consume it. For this shopping cart example app, register the resulting deployed Container Apps' URL in the B2C tenant. For more information, see [ASP.NET Core Blazor authentication and authorization](/aspnet/core/blazor/security). > [!IMPORTANT] -> After your Container App is deployed, you'll need to register the app's URL in the B2C tenant. In most production scenarios, you will only need to register the app's URL once as it shouldn't change. +> After the Container App is deployed, register the app's URL in the B2C tenant. In most production scenarios, the app's URL only needs registration once, as it shouldn't change. To help visualize how the app is isolated within the Azure Container Apps environment, see the following diagram: :::image type="content" source="media/azure-container-apps-http-ingress-diagram.png" lightbox="media/azure-container-apps-http-ingress-diagram.png" alt-text="Azure Container Apps HTTP ingress."::: -In the preceding diagram, all inbound traffic to the app is funneled through a secured HTTP ingress. The Azure Container Apps environment contains an app instance, and the app instance contains an ASP.NET Core host, which exposes the Blazor Server and Orleans app functionality. +In the preceding diagram, all inbound traffic to the app funnels through a secured HTTP ingress. The Azure Container Apps environment contains an app instance, and the app instance contains an ASP.NET Core host, exposing the Blazor Server and Orleans app functionality. ## Deploy to Azure Container Apps -To deploy the app to Azure Container Apps, the repository makes use of GitHub Actions. Before this deployment can take place you'll need a few Azure resources and you'll need to configure the GitHub repository correctly. +To deploy the app to Azure Container Apps, the repository uses GitHub Actions. Before this deployment can occur, a few Azure resources are needed, and the GitHub repository must be configured correctly. [!INCLUDE [create-azure-resources](includes/deployment/create-azure-resources.md)] @@ -64,7 +64,7 @@ To deploy the app to Azure Container Apps, the repository makes use of GitHub Ac ### Prepare for Azure deployment -The app will need to be packaged for deployment. In the `Orleans.ShoppingCart.Silos` project we define a `Target` element that runs after the `Publish` step. This will zip the publish directory into a _silo.zip_ file: +Package the app for deployment. In the `Orleans.ShoppingCart.Silos` project, a `Target` element is defined that runs after the `Publish` step. This target zips the publish directory into a _silo.zip_ file: ```xml @@ -73,7 +73,7 @@ The app will need to be packaged for deployment. In the `Orleans.ShoppingCart.Si ``` -There are many ways to deploy a .NET app to Azure Container Apps. In this tutorial, you use GitHub Actions, Azure Bicep, and the .NET and Azure CLIs. Consider the _./github/workflows/deploy.yml_ file in the root of the GitHub repository: +There are many ways to deploy a .NET app to Azure Container Apps. In this tutorial, use GitHub Actions, Azure Bicep, and the .NET and Azure CLIs. Consider the _./github/workflows/deploy.yml_ file in the root of the GitHub repository: ```yml name: Deploy to Azure Container Apps @@ -164,19 +164,19 @@ jobs: run: az logout ``` -The preceding GitHub workflow will: +The preceding GitHub workflow does the following: -- Publish the shopping cart app as a zip file, using the [dotnet publish](../../core/tools/dotnet-publish.md) command. -- Login to Azure using the credentials from the [Create a service principal](#create-a-service-principal) step. -- Evaluate the _acr.bicep_ file and start a deployment group using [az deployment group create](/cli/azure/deployment/group#az-deployment-group-create). -- Get the Azure Container Registry (ACR) login server from the deployment group. -- Login to ACR using the repositories `AZURE_CREDENTIALS` secret. -- Build and publish the silo image to the ACR. -- Evaluate the _main.bicep_ file and start a deployment group using [az deployment group create](/cli/azure/deployment/group#az-deployment-group-create). -- Deploy the silo -- Logout of Azure. +- Publishes the shopping cart app as a zip file using the [dotnet publish](../../core/tools/dotnet-publish.md) command. +- Logs in to Azure using credentials from the [Create a service principal](#create-a-service-principal) step. +- Evaluates the _acr.bicep_ file and starts a deployment group using [az deployment group create](/cli/azure/deployment/group#az-deployment-group-create). +- Gets the Azure Container Registry (ACR) login server from the deployment group. +- Logs in to ACR using the repository's `AZURE_CREDENTIALS` secret. +- Builds and publishes the silo image to the ACR. +- Evaluates the _main.bicep_ file and starts a deployment group using [az deployment group create](/cli/azure/deployment/group#az-deployment-group-create). +- Deploys the silo. +- Logs out of Azure. -The workflow is triggered by a push to the _main_ branch. For more information, see [GitHub Actions and .NET](../../devops/github-actions-overview.md). +The workflow triggers on a push to the `main` branch. For more information, see [GitHub Actions and .NET](../../devops/github-actions-overview.md). > [!TIP] > If you encounter issues when running the workflow, you might need to verify that the service principal has all the required provider namespaces registered. The following provider namespaces are required: @@ -189,7 +189,7 @@ The workflow is triggered by a push to the _main_ branch. For more information, > > For more information, see [Resolve errors for resource provider registration](/azure/azure-resource-manager/troubleshooting/error-register-resource-provider?tabs=azure-cli). -Azure imposes naming restrictions and conventions for resources. You need to update the _deploy.yml_ file values for the following: +Azure imposes naming restrictions and conventions for resources. Update the values in the _deploy.yml_ file for the following environment variables: - `UNIQUE_APP_NAME` - `SILO_IMAGE_NAME` @@ -202,12 +202,12 @@ For more information, see [Naming rules and restrictions for Azure resources](/a ## Explore the Bicep templates -When the `az deployment group create` command is run, it will evaluate a given _.bicep_ file reference. This file contains declarative information that details the Azure resources you want to deploy. One way to think of this step is that it _provisions_ all of the resources for deployment. +When the `az deployment group create` command runs, it evaluates a given _.bicep_ file reference. This file contains declarative information detailing the Azure resources to deploy. Think of this step as _provisioning_ all resources for deployment. > [!IMPORTANT] > If you're using Visual Studio Code, the Bicep authoring experience is improved when using the [Bicep Extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-Bicep). -The first Bicep file that is evaluated is the _acr.bicep_ file. This file contains the Azure Container Registry (ACR) login server resource details: +The first Bicep file evaluated is the _acr.bicep_ file. This file contains the Azure Container Registry (ACR) login server resource details: ```bicep param location string = resourceGroup().location @@ -227,7 +227,7 @@ output acrLoginServer string = acr.properties.loginServer output acrName string = acr.name ``` -This bicep file outputs the ACR login server and corresponding name. The next Bicep file encountered contains more than just a single `resource`. Consider the _main.bicep_ file comprised primarily of delegating `module` definitions: +This Bicep file outputs the ACR login server and corresponding name. The next Bicep file encountered contains more than just a single `resource`. Consider the _main.bicep_ file, which consists primarily of delegating `module` definitions: ```bicep param appName string @@ -292,16 +292,16 @@ module siloModule 'container-app.bicep' = { output acaUrl string = siloModule.outputs.acaUrl ``` -The preceding Bicep file: +The preceding Bicep file does the following: -- References an `existing` ACR resource, for more information, see [Azure Bicep: Existing resources](/azure/azure-resource-manager/bicep/existing-resource). -- Defines a `module env` that delegates out to the _environment.bicep_ definition file. -- Defines a `module storageModule` that delegates out to the _storage.bicep_ definition file. -- Declares several shared `envVars` that are used by the silo module. -- Defines a `module siloModule` that delegates out to the _container-app.bicep_ definition file. -- Outputs the ACA URL (this could potentially be used to update an existing AAD B2C app registration's redirect URI). +- References an `existing` ACR resource. For more information, see [Azure Bicep: Existing resources](/azure/azure-resource-manager/bicep/existing-resource). +- Defines a `module env` that delegates to the _environment.bicep_ definition file. +- Defines a `module storageModule` that delegates to the _storage.bicep_ definition file. +- Declares several shared `envVars` used by the silo module. +- Defines a `module siloModule` that delegates to the _container-app.bicep_ definition file. +- Outputs the ACA URL (potentially used to update an existing AAD B2C app registration's redirect URI). -The _main.bicep_ delegates out to several other Bicep files. The first is the _environment.bicep_ file: +The _main.bicep_ delegates to several other Bicep files. The first is the _environment.bicep_ file: ```bicep param operationalInsightsName string @@ -351,7 +351,7 @@ output appInsightsInstrumentationKey string = appInsights.properties.Instrumenta output appInsightsConnectionString string = appInsights.properties.ConnectionString ``` -This bicep file defines the Azure Log Analytics and Application Insights resources. The `appInsights` resource is a `web` type, and the `logs` resource is a `PerGB2018` type. Both the `appInsights` resource and the `logs` resource are provisioned in the resource group's location. The `appInsights` resource is linked to the `logs` resource via the `WorkspaceResourceId` property. There are three outputs defined in this bicep, used later by the Container Apps `module`. Next, let's look at the _storage.bicep_ file: +This Bicep file defines the Azure Log Analytics and Application Insights resources. The `appInsights` resource is a `web` type, and the `logs` resource is a `PerGB2018` type. Both `appInsights` and `logs` resources are provisioned in the resource group's location. The `appInsights` resource links to the `logs` resource via the `WorkspaceResourceId` property. This Bicep file defines three outputs used later by the Container Apps `module`. Next, consider the _storage.bicep_ file: ```bicep param name string @@ -378,7 +378,7 @@ The preceding Bicep file defines the following: - Two parameters for the resource group name and the app name. - The `resource storage` definition for the storage account. -- A single `output` that constructs the connection string for the storage account. +- A single `output` constructing the connection string for the storage account. The last Bicep file is the _container-app.bicep_ file: @@ -438,15 +438,15 @@ resource containerApp 'Microsoft.App/containerApps@2022-03-01' = { output acaUrl string = containerApp.properties.configuration.ingress.fqdn ``` -The aforementioned Visual Studio Code extension for Bicep includes a visualizer. All of these Bicep files are visualized as follows: +The aforementioned Visual Studio Code extension for Bicep includes a visualizer. All these Bicep files are visualized as follows: :::image type="content" source="media/shopping-cart-container-app-Bicep-visualizer.png" alt-text="Orleans: Shopping cart sample app Bicep provisioning visualizer rendering." lightbox="media/shopping-cart-container-app-Bicep-visualizer.png"::: ## Summary -As you update the source code and `push` changes to the `main` branch of the repository, the _deploy.yml_ workflow will run. It provisions the Azure resources defined in the Bicep files and deploys the application. Revisions are automatically registered in your Azure Container Registry. +As source code is updated and changes are `push`ed to the `main` branch of the repository, the _deploy.yml_ workflow runs. It provisions the Azure resources defined in the Bicep files and deploys the application. Revisions are automatically registered in the Azure Container Registry. -In addition to the visualizer from the Bicep extension, the Azure portal resource group page would look similar to the following example after provisioning and deploying the application: +In addition to the visualizer from the Bicep extension, the Azure portal resource group page looks similar to the following example after provisioning and deploying the application: :::image type="content" source="media/shopping-cart-aca-resources.png" alt-text="Azure Portal: Orleans shopping cart sample app resources for Azure Container Apps." lightbox="media/shopping-cart-aca-resources.png"::: diff --git a/docs/orleans/deployment/docker-deployment.md b/docs/orleans/deployment/docker-deployment.md index 188ba5511814f..2964685306dc3 100644 --- a/docs/orleans/deployment/docker-deployment.md +++ b/docs/orleans/deployment/docker-deployment.md @@ -1,48 +1,50 @@ --- title: Docker deployment description: Learn how to deploy Orleans apps with Docker. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: how-to +ms.custom: devops --- # Docker deployment > [!TIP] -> Even if you're familiar with Docker or Orleans, it's recommended you to read this article to the end to avoid problems you might face that have workarounds. +> Even if familiar with Docker or Orleans, reading this article to the end is recommended to avoid potential problems with known workarounds. -This article and its sample are a work in progress. Any feedback, PR, or suggestion is welcome. +This article and its sample are a work in progress. Feedback, PRs, or suggestions are welcome. ## Deploy Orleans solutions to Docker -Deploying Orleans to Docker can be tricky given the way Docker orchestrators and clustering stacks were designed. The most complicated thing is to understand the concept of _Overlay Network_ from Docker Swarm and Kubernetes networking model. +Deploying Orleans to Docker can be tricky due to the design of Docker orchestrators and clustering stacks. The most complicated part is understanding the concept of _Overlay Network_ from Docker Swarm and the Kubernetes networking model. -Docker containers and networking models were designed to run mostly stateless and immutable containers. So, spinning up a cluster running node.js or Nginx applications is pretty easy. However, if you try to use something more elaborate, like a real clustered or distributed application (like Orleans-based ones) you will eventually have trouble setting it up. It is possible, but not as simple as web-based applications. +Docker containers and networking models were primarily designed for running stateless and immutable containers. Spinning up a cluster running Node.js or Nginx applications is fairly easy. However, using something more elaborate, like a truly clustered or distributed application (such as one based on Orleans), might present setup difficulties. It's possible, but not as straightforward as deploying web-based applications. -Docker clustering consists of putting together multiple hosts to work as a single pool of resources, managed using a _Container Orchestrator_. _Docker Inc._ provide **Swarm** as their option for Container Orchestration while _Google_ has **Kubernetes** (aka **K8s**). There are other Orchestrators like **DC/OS**, **Mesos**, but in this document, we will talk about Swarm and K8s as they are more widely used. +Docker clustering involves grouping multiple hosts to work as a single resource pool, managed using a _Container Orchestrator_. _Docker Inc._ provides **Swarm** as their option, while _Google_ offers **Kubernetes** (also known as **K8s**). Other orchestrators like **DC/OS** and **Mesos** exist, but this document focuses on Swarm and K8s as they are more widely used. -The same grain interfaces and implementation that run anywhere Orleans is already supported will run on Docker containers as well. No special considerations are needed to be able to run your application in Docker containers. +The same grain interfaces and implementations that run anywhere Orleans is supported also run on Docker containers. No special considerations are needed to run an Orleans application in Docker containers. -The concepts discussed here can be used on both .NET Core and .NET 4.6.1 flavors of Orleans but to illustrate the cross-platform nature of Docker and .NET Core, we are going to focus on the example considering you are using .NET Core. Platform-specific (Windows/Linux/OSX) details may be provided in this article. +The concepts discussed here apply to both .NET Core and .NET Framework 4.6.1 versions of Orleans. However, to illustrate the cross-platform nature of Docker and .NET Core, examples using .NET Core are focused on. Platform-specific details (Windows/Linux/macOS) might be provided where necessary. ## Prerequisites -This article assumes that you have the following prerequisites installed: +This article assumes the following prerequisites are installed: -* [Docker](https://www.docker.com/community-edition#/download) - Docker4X has an easy-to-use installer for the major supported platforms. It contains Docker engine and also Docker Swarm. -* [Kubernetes (K8s)](https://kubernetes.io/docs/tutorials/stateless-application/hello-minikube/) - Google's offer for Container Orchestration. It contains guidance to install _Minikube_ (a local deployment of K8s) and _kubectl_ along with all its dependencies. -* [.NET](https://dotnet.microsoft.com/download) - Cross-platform flavor of .NET -* [Visual Studio Code (VSCode)](https://code.visualstudio.com/) - You can use whatever IDE you want. VSCode is cross-platform so we are using it to ensure it works on all platforms. Once you installed VSCode, install the [C# extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp). +- [Docker](https://www.docker.com/community-edition#/download): Docker4X has an easy-to-use installer for major supported platforms. It contains the Docker engine and Docker Swarm. +- [Kubernetes (K8s)](https://kubernetes.io/docs/tutorials/stateless-application/hello-minikube/): Google's container orchestration offering. It includes guidance for installing _Minikube_ (a local K8s deployment) and _kubectl_ along with dependencies. +- [.NET](https://dotnet.microsoft.com/download): Cross-platform flavor of .NET. +- [Visual Studio Code (VSCode)](https://code.visualstudio.com/): Any preferred IDE can be used. VSCode is used here because it's cross-platform, ensuring examples work on all platforms. After installing VSCode, install the [C# extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp). > [!IMPORTANT] -> You are not required to have Kubernetes installed if you are not going to use it. Docker4X installer already includes Swarm so no extra installation is required to use it. +> Kubernetes installation isn't required if not using it. The Docker4X installer already includes Swarm, so no extra installation is needed for Swarm. > [!NOTE] -> On Windows, Docker installer will enable Hyper-V at the installation process. As this article and its examples are using .NET Core, the container images used are based on **Windows Server NanoServer**. If you don't plan to use .NET Core and will target .NET 4.6.1 full framework, the image used should be **Windows Server Core** and the 1.4+ version of Orleans (which supports only .NET full framework). +> On Windows, the Docker installer enables Hyper-V during installation. Since this article and its examples use .NET Core, the container images used are based on **Windows Server NanoServer**. If planning to use .NET Framework 4.6.1 instead, use images based on **Windows Server Core** and Orleans version 1.4+ (which supports only .NET Framework). -## Create Orleans solution +## Create an Orleans solution -The following instructions show how to create a regular Orleans solution using the new `dotnet` tooling. +The following instructions show how to create a standard Orleans solution using the `dotnet` tooling. -Please adapt the commands to whatever is appropriate in your platform. Also, the directory structure is just a suggestion. Please adapt it to your needs. +Adapt the commands as appropriate for the platform. The directory structure is just a suggestion; adapt it as needed. ```bash mkdir Orleans-Docker @@ -66,9 +68,9 @@ dotnet add src/OrleansGrains/OrleansGrains.csproj reference src/OrleansGrainInte dotnet add src/OrleansSilo/OrleansSilo.csproj reference src/OrleansGrains/OrleansGrains.csproj ``` -What we did so far was just boilerplate code to create the solution structure and projects, and add references between projects. Nothing different than a regular Orleans project. +So far, only boilerplate code for the solution structure and projects has been created, and references added between them. This is no different from setting up a regular Orleans project. -By the time this article was written, Orleans 2.0 (which is the only version that supports .NET Core and cross-platform) is in Technology Preview so its NuGet packages are hosted in a MyGet feed and not published to Nuget.org official feed. To install the preview NuGet packages, we will use `dotnet` CLI forcing the source feed and version from MyGet: +At the time this article was written, Orleans 2.0 (supporting .NET Core and cross-platform development) was in Technology Preview. Its NuGet packages were hosted on a MyGet feed, not the official NuGet.org feed. To install the preview NuGet packages, use the `dotnet` CLI, forcing the source feed and version from MyGet: ```bash dotnet add src/OrleansClient/OrleansClient.csproj package Microsoft.Orleans.Core -s https://dotnet.myget.org/F/orleans-prerelease/api/v3/index.json -v 2.0.0-preview2-201705020000 @@ -79,17 +81,17 @@ dotnet add src/OrleansSilo/OrleansSilo.csproj package Microsoft.Orleans.OrleansR dotnet restore ``` -Ok, now you have all the basic dependencies to run a simple Orleans application. Note that so far, nothing changed from your regular Orleans application. Now, let's add some code so we can do something with it. +Okay, all basic dependencies to run a simple Orleans application are now in place. Note that nothing has changed from a regular Orleans application setup up to this point. Now, let's add some code to make it functional. -## Implement your Orleans application +## Implement the Orleans application -Assuming that you are using **VSCode**, from the solution directory, run `code .`. That will open the directory in **VSCode** and load the solution. +Assuming **VSCode** is used, run `code .` from the solution directory. This command opens the directory in **VSCode** and loads the solution. -This is the solution structure we just created previously. +This is the solution structure created previously. ![Visual Studio Code: Explorer with Program.cs selected.](docker-orleans-solution.png) -We also added _Program.cs_, _OrleansHostWrapper.cs_, _IGreetingGrain.cs_ and _GreetingGrain.cs_ files to the interfaces and grain projects respectively, and here is the code for those files: +_Program.cs_, _OrleansHostWrapper.cs_, _IGreetingGrain.cs_, and _GreetingGrain.cs_ files were also added to the interfaces and grain projects, respectively. Here is the code for those files: _IGreetingGrain.cs_: @@ -318,15 +320,15 @@ namespace OrleansClient } ``` -We are not going into details about the grain implementation here since it is out of the scope of this article. Please check other documents related to it. Those files are essentially a minimal Orleans application and we will start from it to move forward with the remaining of this article. +The grain implementation details aren't covered here, as it's outside the scope of this article. Refer to other relevant documents for more information. These files represent a minimal Orleans application, serving as the starting point for the rest of this article. -In this article, we are using `OrleansAzureUtils` membership provider but you can use any other already supported by Orleans. +This article uses the `OrleansAzureUtils` membership provider, but any other Orleans-supported provider can be used. ## The Dockerfile -To create your container, Docker uses images. For more details on how to create your own, you can check [Docker documentation](https://docs.docker.com/engine/userguide/). In this article, we are going to use official [Microsoft images](https://hub.docker.com/r/microsoft/dotnet/). Based on the target and development platforms, you need to pick the appropriate image. In this article, we are using `microsoft/dotnet:1.1.2-sdk` which is a Linux-based image. You can use `microsoft/dotnet:1.1.2-sdk-nanoserver` for Windows for example. Pick one that suits your needs. +Docker uses images to create containers. For more details on creating custom images, check the [Docker documentation](https://docs.docker.com/engine/userguide/). This article uses official [Microsoft images](https://hub.docker.com/r/microsoft/dotnet/). Based on the target and development platforms, pick the appropriate image. `microsoft/dotnet:1.1.2-sdk`, a Linux-based image, is used here. For Windows, `microsoft/dotnet:1.1.2-sdk-nanoserver` could be used, for example. Choose the one that suits the needs. -> **Note for Windows users**: As previously mentioned, to be cross-platform, we are using .NET Core and Orleans Technical preview 2.0 in this article. If you want to use Docker on Windows with the fully released Orleans 1.4+, you need to use the images that are based on Windows Server Core since NanoServer and Linux-based images, only support .NET Core. +> **Note for Windows users**: As mentioned earlier, to maintain cross-platform compatibility, .NET Core and Orleans Technical Preview 2.0 are used in this article. To use Docker on Windows with the fully released Orleans 1.4+, use images based on Windows Server Core, since NanoServer and Linux-based images only support .NET Core. _Dockerfile.debug_: @@ -343,9 +345,9 @@ WORKDIR /app ENTRYPOINT ["tail", "-f", "/dev/null"] ``` -This _Dockerfile_ essentially downloads and installs the VSdbg debugger and starts an empty container, keeping it alive forever so we don't need to tear down/up while debugging. +This _Dockerfile_ essentially downloads and installs the VSdbg debugger and starts an empty container, keeping it alive indefinitely so it doesn't need tearing down and bringing up repeatedly during debugging. -Now, for production, the image is smaller since it contains only the .NET Core runtime and not the whole SDK, and the dockerfile is a bit simpler: +Now, for production, the image is smaller because it contains only the .NET Core runtime, not the entire SDK. The Dockerfile is also simpler: _Dockerfile_: @@ -358,14 +360,14 @@ COPY . /app ## The docker-compose file -The `docker-compose.yml` file, essentially defines (within a project) a set of services and its dependencies at the service level. Each service contains one or more instances of a given container, which is based on the images you selected on your Dockerfile. More details on the `docker-compose` can be found on [docker-compose documentation](https://docs.docker.com/compose/). +The `docker-compose.yml` file defines a set of services and their dependencies within a project at the service level. Each service contains one or more instances of a given container, based on the images selected in the Dockerfile. Find more details about `docker-compose` in the [docker-compose documentation](https://docs.docker.com/compose/). -For an Orleans deployment, a common use case is to have a `docker-compose.yml` which contains two services. One for Orleans Silo, and the other for Orleans Client. The Client would have a dependency on the Silo and that means, it will only start after the Silo service is up. Another case is to add a storage/database service/container, like for example SQL Server, which should start first before the client and the silo, so both services should take a dependency on it. +For an Orleans deployment, a common use case involves a `docker-compose.yml` file containing two services: one for the Orleans Silo and another for the Orleans Client. The Client service depends on the Silo service, meaning it only starts after the Silo service runs. Another scenario might involve adding a storage or database service/container (like SQL Server), which should start before both the client and the silo. In this case, both client and silo services would depend on the database service. > [!NOTE] -> Before you read further, please note that _indentation_ **matters** in `docker-compose` files. So pay attention to it if you have any problems. +> Before reading further, note that _indentation_ **matters** in `docker-compose` files. Pay attention to it if problems arise. -Here is how we will describe our services for this article: +Here's how the services are described for this article: _docker-compose.override.yml_ (Debug): @@ -407,39 +409,39 @@ services: image: orleans-silo ``` -In production, we don't map the local directory, and neither we have the `build:` action. The reason is that in production, the images should be built and pushed to your own Docker Registry. +In production, the local directory isn't mapped, nor is the `build:` action included. The reason is that in a production environment, images should be built and pushed to a private Docker Registry. ## Put everything together -Now we have all the moving parts required to run your Orleans Application, we are going to put it together so we can run our Orleans solution inside Docker (Finally!). +Now that all necessary components are ready, let's put them together to run the Orleans solution inside Docker. > [!IMPORTANT] > The following commands should be performed from the solution directory. -First, let's make sure we restore all NuGet packages from our solution. You only need to do it once. You are only required to do it again if you change any package dependency on your project. +First, ensure all NuGet packages for the solution are restored. This typically needs doing only once, unless package dependencies change. ```dotnetcli dotnet restore ``` -Now, let's build our solution using `dotnet` CLI as usual and publish it to an output directory: +Now, build the solution using the `dotnet` CLI as usual and publish it to an output directory: ```dotnetcli dotnet publish -o ./bin/PublishOutput ``` > [!TIP] -> We are using `publish` here instead of build, to avoid problems with our dynamically loaded assemblies in Orleans. We are still looking for a better solution for it. +> `publish` is used here instead of `build` to avoid problems with dynamically loaded assemblies in Orleans. A better solution is still being sought. -With the application built and published, you need to build your Dockerfile images. This step is only required to be performed once per project and should be only performed again if you change the Dockerfile, docker-compose, or for any reason you cleaned up your local image registry. +With the application built and published, build the Docker images using the Dockerfiles. This step typically needs performing only once per project. It should only be needed again if the Dockerfile or docker-compose file changes, or if the local image registry is cleaned up for any reason. ```shell docker-compose build ``` -All the images used in both `Dockerfile` and `docker-compose.yml` are pulled from the registry and cached on your development machine. Your images are built, and you are all set to run. +All base images used in both `Dockerfile` and `docker-compose.yml` are pulled from the registry and cached on the development machine. The application images are built, and everything is ready to run. -Now, let's run it! +Now, let's run the application! ```shell # docker-compose up -d @@ -451,7 +453,7 @@ Creating orleansdocker_orleans-client_1 ... done # ``` -Now if you run a `docker-compose ps`, you will see 2 containers running for the `orleansdocker` project: +Now, running `docker-compose ps` shows two containers running for the `orleansdocker` project: ```shell # docker-compose ps @@ -462,13 +464,13 @@ orleansdocker_orleans-silo_1 tail -f /dev/null Up ``` > [!NOTE] -> If you are on Windows, and your container is using a Windows image as a base, the **Command** column will show you the PowerShell relative command to a `tail` on *NIX systems so the container will keep up the same way. +> If on Windows and the container uses a Windows base image, the **Command** column shows the PowerShell equivalent command to `tail` on *NIX systems, keeping the container running similarly. -Now that you have your containers up, you don't need to stop it every time you want to start your Orleans application. All you need is to integrate your IDE to debug the application inside the container which was previously mapped in your `docker-compose.yml`. +Now that the containers are running, stopping them isn't necessary every time the Orleans application needs starting. Just integrate the IDE to debug the application inside the container, which was previously mapped in `docker-compose.yml`. ## Scaling -Once you have your compose project running, you can easily scale up or down your application using `docker-compose scale` command: +Once the compose project runs, easily scale the application up or down using the `docker-compose scale` command: ```shell # docker-compose scale orleans-silo=15 @@ -503,7 +505,7 @@ Creating orleansdocker_orleans-silo_14 Creating orleansdocker_orleans-silo_13 ``` -After a few seconds, you will see the services scaled to the specific number of instances you requested. +After a few seconds, the services scale to the specific number of instances requested. ```shell # docker-compose ps @@ -528,30 +530,30 @@ orleansdocker_orleans-silo_9 tail -f /dev/null Up ``` > [!IMPORTANT] -> The `Command` column on those examples are showing the `tail` command just because we are using the debugger container. If we were in production, it would be showing `dotnet OrleansSilo.dll` for example. +> The `Command` column in these examples shows the `tail` command because the debugger container is used. In production, it would show `dotnet OrleansSilo.dll`, for example. -## Docker swarm +## Docker Swarm -Docker clustering stack is called **Swarm**, for more information see, [Docker Swarm](https://docs.docker.com/engine/swarm). +Docker's clustering stack is called **Swarm**. For more information, see [Docker Swarm](https://docs.docker.com/engine/swarm). -To run this article in a `Swarm` cluster, you don't have any extra work. When you run `docker-compose up -d` in a `Swarm` node, it will schedule containers based on the configured rules. The same applies to other Swarm-based services like [Azure ACS](/azure/aks) (in Swarm mode) and [AWS ECS Container Service](https://aws.amazon.com/ecs/). All you need to do is to deploy your `Swarm` cluster before deploying your **dockerized** Orleans application. +To run the application described in this article in a `Swarm` cluster, no extra work is needed. Running `docker-compose up -d` on a `Swarm` node schedules containers based on configured rules. The same applies to other Swarm-based services like [Azure Kubernetes Service (AKS)](/azure/aks) (in Swarm mode) and [AWS Elastic Container Service (ECS)](https://aws.amazon.com/ecs/). Just deploy the `Swarm` cluster before deploying the **dockerized** Orleans application. > [!NOTE] -> If you're using a Docker engine with the Swarm mode that already has support for `stack`, `deploy`, and `compose` v3, a better approach to deploy your solution is `docker stack deploy -c docker-compose.yml `. Just keep in mind that it requires v3 compose file to support your Docker engine, and the majority of hosted services like Azure and AWS still use v2 and older engines. +> If using a Docker engine with Swarm mode supporting `stack`, `deploy`, and `compose` v3, a better approach to deploy the solution is `docker stack deploy -c docker-compose.yml `. Keep in mind this requires a v3 compose file compatible with the Docker engine. Many hosted services like Azure and AWS still use v2 and older engines. ## Google Kubernetes (K8s) -If you plan to use [Kubernetes](https://kubernetes.io/) to host Orleans, there is a community-maintained clustering provider available at [OrleansContrib\Orleans.Clustering.Kubernetes](https://github.com/OrleansContrib/Orleans.Clustering.Kubernetes). There you can find documentation and samples on how to host Orleans in Kubernetes seamlessly using the provider. +If planning to use [Kubernetes](https://kubernetes.io/) to host Orleans, a community-maintained clustering provider is available at [OrleansContrib\Orleans.Clustering.Kubernetes](https://github.com/OrleansContrib/Orleans.Clustering.Kubernetes). There, find documentation and samples on hosting Orleans in Kubernetes seamlessly using the provider. ## Debug Orleans inside containers -Now that you know how to run Orleans in a container from scratch, it's good to leverage one of the most important principles in Docker. Containers are immutable. And they should have (almost) the same image, dependencies, and runtime in development as in production. This ensures the good old statement _"It works on my machine!"_ never happens again. To make that possible, you need to have a way to develop _inside_ the container and that includes having a debugger attached to your application inside the container. +Now that running Orleans in a container from scratch is understood, it's beneficial to leverage one of Docker's most important principles: immutability. Containers should have (almost) the same image, dependencies, and runtime in development as in production. This practice helps prevent the classic _"It works on my machine!"_ problem. To make this possible, a way to develop _inside_ the container is needed, including attaching a debugger to the application running inside it. -There are multiple ways to achieve that using multiple tools. After evaluating several, by the time I wrote this article, I ended up choosing one that looks more simple and is less intrusive in the application. +Multiple ways exist to achieve this using various tools. After evaluating several options at the time of writing, one that seems simpler and less intrusive to the application was chosen. -As mentioned earlier in this article, we are using `VSCode` to develop the sample, so here is how to get the debugger attached to your Orleans Application inside the container. +As mentioned earlier, `VSCode` is used to develop the sample. Here's how to attach the debugger to the Orleans application inside the container: -First, change two files inside your `.vscode` directory in your solution: +First, modify two files inside the `.vscode` directory in the solution: _tasks.json_: @@ -574,7 +576,7 @@ _tasks.json_: } ``` -This file essentially tells `VSCode` that whenever you build the project, it will execute the `publish` command as we manually did earlier. +This file essentially tells `VSCode` that whenever the project builds, it executes the `publish` command, similar to how it was done manually earlier. _launch.json_: @@ -624,4 +626,4 @@ _launch.json_: } ``` -Now you can just build the solution from `VSCode` (which will publish) and start both the Silo and the Client. It will send a `docker exec` command to the running `docker-compose` service instance/container to start the debugger to the application and that's it. You have the debugger attached to the container and use it as if it was a locally running Orleans application. The difference now is that it is inside the container, and once you are done, you can just publish the container to your registry and pull it on your Docker hosts in production. +Now build the solution from `VSCode` (which also publishes) and start both the Silo and Client configurations. VSCode sends a `docker exec` command to the running `docker-compose` service instance/container to start the debugger attached to the application. That's it! The debugger is attached to the container and can be used just like debugging a locally running Orleans application. The key difference is the application runs inside the container. Once development is done, publish the container image to the registry and pull it onto Docker hosts in production. diff --git a/docs/orleans/deployment/handling-failures.md b/docs/orleans/deployment/handling-failures.md index 51122e5618d33..7e1f2864d40e0 100644 --- a/docs/orleans/deployment/handling-failures.md +++ b/docs/orleans/deployment/handling-failures.md @@ -1,80 +1,70 @@ --- title: Failure handling description: Learn how to handle failures in Orleans apps. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Failure handling > [!TIP] -> All of the following guidance in this document is provided to serve as examples and food for thought. You should not think of them as prescriptive solutions to your problems because failure handling is a rather application-specific subject. These patterns and others are only useful if applied with a good knowledge of the concrete case being worked on. +> All guidance in this document serves as examples and food for thought. Don't consider them prescriptive solutions, because failure handling is highly application-specific. These patterns and others are useful only if applied with a good understanding of the specific case. -The hardest thing in programming a distributed system is handling failures. The actor model and the way it works make it much easier to deal with different kinds of failures, but as a developer, you are responsible for dealing with the failure possibilities and appropriately handling them. +Handling failures is often the hardest part of programming a distributed system. The actor model makes dealing with different kinds of failures much easier, but developers are responsible for considering failure possibilities and handling them appropriately. ## Types of failures -When you are coding your grains, all calls are asynchronous and have the potential to go over the network. -Each grain call can fail due to one of the following reasons. +When coding grains, all calls are asynchronous and potentially go over the network. Each grain call can fail for one of the following reasons: -- The grain was activated on a silo that is unavailable at the moment due to a network partition crash or some other reason. If the silo has not been declared dead yet, your request might time out. -- The grain method call can throw an exception signaling that it failed and can not continue its job. -- An activation of the grain doesn't exist and cannot be created because the `OnActivateAsync` method throws an exception or is dead-locked. -- Network failures don't let you communicate with the grain before timeout. -- Other potential reasons +- The grain was activated on a silo currently unavailable due to a network partition, crash, or other reason. If the silo hasn't been declared dead yet, the request might time out. +- The grain method call can throw an exception, signaling failure and inability to continue its job. +- An activation of the grain doesn't exist and cannot be created because the `OnActivateAsync` method throws an exception or deadlocks. +- Network failures prevent communication with the grain before the timeout occurs. +- Other potential reasons. ## Detection of failures -Getting a reference to a grain always succeeds and is a local operation. -However, method calls can fail, and when they do, you get an exception. -You can catch the exception at any level you need and they are propagated even across silos. +Getting a reference to a grain always succeeds and is a local operation. However, method calls can fail. When they do, an exception is received. Catch the exception at any necessary level; Orleans propagates them even across silos. ## Recover from failures -Part of the recovery job is automatic in Orleans and if a grain is not accessible anymore, Orleans will reactivate it in the next method call. -The thing you need to handle and make sure is correct in the context of your application is the state. -A grain's state can be partially updated or the operation might be something that should be done across multiple grains and is carried on partially. +Part of the recovery job is automatic in Orleans. If a grain is no longer accessible, Orleans reactivates it on the next method call. The part needing handling and ensuring correctness within the application's context is the state. A grain's state might be partially updated, or an operation might involve multiple grains and only complete partially. -After you see a grain operation fail you can do one or more of the following. +After observing a grain operation failure, take one or more of the following actions: -- Simply retry your action, especially if it doesn't involve any state changes which might be half done. -This is by far the most typical case. -- Try to fix/reset the partially changed state by calling a method that resets the state to the last known correct state or just reads it from storage by calling `ReadStateAsync`. -- Reset the state of all related activations as well to ensure a clean state for all of them. -- Perform multi-grain state manipulations using a Process Manager or database transaction to make sure it's either done completely or nothing is changed to avoid the state being partially updated. +- **Retry the action**: Often suitable, especially if the action doesn't involve state changes that might be left half-done. This is the most common approach. +- **Fix/reset state**: Try to correct the partially changed state. Do this by calling a method that resets the state to the last known correct state or by simply reading the correct state from storage using `ReadStateAsync`. +- **Reset related states**: Reset the state of all related activations as well to ensure a consistent state across all involved grains. +- **Use transactions or process managers**: Perform multi-grain state manipulations using a Process Manager pattern or database transactions. This ensures the operation either completes entirely or leaves the state unchanged, avoiding partial updates. -Depending on your application, the retry logic might follow a simple or complex pattern, and you might have to do other stuff like notifying external systems and other things, but generally, you either have to retry your action, restart the grain/grains involved, or stop responding until something which is not available becomes available. +Depending on the application, the retry logic might follow a simple or complex pattern. Other actions might also be needed, like notifying external systems. Generally, the options are to retry the action, restart the involved grain(s), or stop responding until a required resource becomes available again. -If you have a grain responsible for database saves and the database is not available, you simply have to fail any request until the database comes back online. If your operation can be retried at the user's will, like a failure of saving a comment in a comment grain, you can retry when the user presses the retry button (until a certain number of times to not saturate the network). Specific details of how to do it are application-specific, but the possible strategies are the same. +For example, if a grain is responsible for database saves and the database is unavailable, simply fail incoming requests until the database is back online. If an operation can be retried at the user's discretion (like failing to save a comment), allow the user to retry via a button press (perhaps limiting retries to avoid network saturation). Specific implementation details depend on the application, but the underlying strategies remain the same. ## Strategy parameters -As described in the section above, the strategy you choose depends on the application and context. -Strategies usually have parameters that have to be decided at the application level. -For example, if no other processes depend on an action, then you might decide to retry that action a maximum of 5 times per minute until it eventually completes. But you would have to process a user's Login grain request before processing any other requests from that user. -If the login action is not working, then you cannot continue. +As described above, the chosen strategy depends on the application and specific context. Strategies usually involve parameters decided at the application level. For example, if no other processes depend on an action, decide to retry that action a maximum of 5 times per minute until completion. However, successfully processing a user's Login grain request might be necessary before processing other requests from that user. If the login action fails, proceeding with other actions for that user isn't possible. -There is a guide [in the Azure documentation](/azure/architecture/patterns) about good patterns and practices for the cloud which applies to Orleans as well, in most cases. +The Azure Architecture Center provides a guide on [Cloud Design Patterns](/azure/architecture/patterns), which often apply to Orleans applications as well. -## A fairly complex example +## A complex example -Because in Orleans grains are activated and deactivated automatically and you don't handle their life-cycle, you usually only deal with making sure that grain state is correct and actions are being started and finished correctly with each other. Knowing the dependencies between grains and actions they perform is a big step toward understanding how to handle failure in any complex system. -If you need to store relations among grains, you can simply do it and it is a widely followed practice, too. +Because Orleans activates and deactivates grains automatically, their lifecycle isn't handled directly. Instead, focus typically lies on ensuring grain state correctness and that actions involving multiple grains start and finish correctly relative to each other. Understanding dependencies between grains and their actions is crucial for handling failures in any complex system. Storing relationships between grains is certainly possible and a common practice. -As an example, let's say we have a `GameManager` grain that starts and stops `Game` grains and adds `Player` grains to the games. If my `GameManager`grain fails to do its action regarding starting a game, the related game belonging to it should fail to do its `Start()` as well and the manager can do this for the game by doing orchestration. Managing memory in Orleans is automatic and the system deals with it, you only need to make sure that the game starts and only if the manager can do its `Start()` as well. You can achieve this by either calling the related methods in a sequential manner or by doing them in parallel and resetting the state of all involved grains if any of them fail. +As an example, consider a `GameManager` grain starting and stopping `Game` grains and adding `Player` grains to games. If the `GameManager` fails while starting a game, the corresponding `Game` grain should also fail its `Start()` operation. The manager can orchestrate this. Memory management in Orleans is automatic; just ensure the game starts successfully if, and only if, the manager also successfully completes its `Start()` related actions. Achieve this coordination by calling related methods sequentially or executing them in parallel and resetting the state of all involved grains if any step fails. -If one of the games receives a call, it will be reactivated automatically, so if you need the manager to manage the game grains, then all calls to the game which are related to management should go through the `GameManager`. If you need orchestration among grains, Orleans doesn't solve it "automagically" for you and you need to do your orchestration. But the fact that you are not dealing with creating/destroying grains means you don't need to worry about resource management. -You don't need to answer any of these questions: +If a game grain receives a call later, Orleans reactivates it automatically. Therefore, if the manager needs to oversee game grains, all management-related calls to the game should go through the `GameManager`. If orchestration among grains is needed, Orleans doesn't solve it "automagically"; the orchestration logic must be implemented. However, because creating or destroying grains isn't handled directly, resource management isn't a concern. Questions like these don't need answering: -- Where should I create my supervision tree? -- which grains should I register to be addressable by name? -- Is grain X alive so I can send it a message? +- Where should the supervision tree be created? +- Which grains should be registered to be addressable by name? +- Is grain X alive so a message can be sent to it? -So, the game start example can be summarized like this: +So, the game start example can be summarized as follows: -- `GameManager` asks the `Game` grain to start -- `Game` grain adds the `Player` grains to itself -- `Game` asks `Player` grains to add the game to themselves -- `Game` sets its state to be started. +- `GameManager` asks the `Game` grain to start. +- `Game` grain adds the `Player` grains to itself. +- `Game` asks `Player` grains to add the game to themselves. +- `Game` sets its state to started. - `GameManager` adds the game to its list of games. -Now, if a player fails to add the game to itself, you don't need to kill all players and the game and start over. You simply reset the state of the other players who added the game to themselves, reset the state of the `Game` and `GameManager` (if required), and redo your work or declare a failure. If you can deal with adding the game to the player, later on, you can continue and retry doing that again in a reminder or at some other game call like the `Finish()` method of the game. +Now, if a player grain fails to add the game to itself, killing all players and the game and starting over isn't necessarily required. Simply reset the state of the other players who successfully added the game, reset the state of the `Game` and `GameManager` (if required), and either retry the operation or declare a failure. If adding the game to the player later is acceptable, continue the process and retry adding the game to that specific player using a reminder or during another game call, like the game's `Finish()` method. diff --git a/docs/orleans/deployment/includes/deployment/create-azure-resources.md b/docs/orleans/deployment/includes/deployment/create-azure-resources.md index bb4cc5e32fdb3..87966b5d3c6a9 100644 --- a/docs/orleans/deployment/includes/deployment/create-azure-resources.md +++ b/docs/orleans/deployment/includes/deployment/create-azure-resources.md @@ -1,7 +1,7 @@ -Before deploying the app, you need to create an Azure Resource Group (or you could choose to use an existing one). To create a new Azure Resource Group, use one of the following articles: +Before deploying the app, create an Azure Resource Group (or you can use an existing one). To create a new Azure Resource Group, use one of the following articles: - [Azure Portal](/azure/azure-resource-manager/management/manage-resource-groups-portal#create-resource-groups) - [Azure CLI](/azure/azure-resource-manager/management/manage-resource-groups-cli#create-resource-groups) - [Azure PowerShell](/azure/azure-resource-manager/management/manage-resource-groups-powershell#create-resource-groups) -Make note of the resource group name you choose, you'll need it later to deploy the app. +Make note of the resource group name you choose; you need it later to deploy the app. diff --git a/docs/orleans/deployment/includes/deployment/create-github-secret.md b/docs/orleans/deployment/includes/deployment/create-github-secret.md index eca28299c1143..77520de0ddc19 100644 --- a/docs/orleans/deployment/includes/deployment/create-github-secret.md +++ b/docs/orleans/deployment/includes/deployment/create-github-secret.md @@ -1,8 +1,8 @@ ### Create a GitHub secret -GitHub provides a mechanism for creating encrypted secrets. The secrets that you create are available to use in GitHub Actions workflows. You're going to see how GitHub Actions can be used to automate the deployment of the app, in conjunction with Azure :::no-loc text="Bicep":::. :::no-loc text="Bicep"::: is a domain-specific language (DSL) that uses a declarative syntax to deploy Azure resources. For more information, see [What is Bicep](/azure/azure-resource-manager/bicep/overview?tabs=bicep). Using the output from the [Create a service principal](#create-a-service-principal) step, you'll need to create a GitHub secret named `AZURE_CREDENTIALS` with the JSON-formatted credentials. +GitHub provides a mechanism for creating encrypted secrets. The secrets you create are available for use in GitHub Actions workflows. You'll see how to use GitHub Actions to automate the app's deployment in conjunction with Azure Bicep. Bicep is a domain-specific language (DSL) that uses declarative syntax to deploy Azure resources. For more information, see [What is Bicep?](/azure/azure-resource-manager/bicep/overview?tabs=bicep). Using the output from the [Create a service principal](#create-a-service-principal) step, you need to create a GitHub secret named `AZURE_CREDENTIALS` with the JSON-formatted credentials. -Within the GitHub repository, select **Settings** > **Secrets** > **Create a new secret**. Enter the name `AZURE_CREDENTIALS` and paste the JSON credentials from the previous step into the **Value** field. +Within your GitHub repository, select **Settings** > **Secrets** > **Create a new secret**. Enter the name `AZURE_CREDENTIALS` and paste the JSON credentials from the previous step into the **Value** field. :::image type="content" source="../../media/github-secret.png" alt-text="GitHub Repository: Settings > Secrets" lightbox="../../media/github-secret.png"::: diff --git a/docs/orleans/deployment/includes/deployment/create-service-principal.md b/docs/orleans/deployment/includes/deployment/create-service-principal.md index 78ca8b69c7757..27c618457cdf4 100644 --- a/docs/orleans/deployment/includes/deployment/create-service-principal.md +++ b/docs/orleans/deployment/includes/deployment/create-service-principal.md @@ -1,13 +1,13 @@ ### Create a service principal -To automate the deployment of the app, you'll need to create a service principal. This is a Microsoft account that has permission to manage Azure resources on your behalf. +To automate the app's deployment, you need to create a service principal. This is a Microsoft account that has permission to manage Azure resources on your behalf. ```azurecli az ad sp create-for-rbac --sdk-auth --role Contributor \ --name "" --scopes /subscriptions/ ``` -The JSON credentials created will look similar to the following, but with actual values for your client, subscription, and tenant: +The JSON credentials created look similar to the following, but with actual values for your client, subscription, and tenant: ```json { diff --git a/docs/orleans/deployment/index.md b/docs/orleans/deployment/index.md index cbba5f1098d6e..a1ff36d8fedaf 100644 --- a/docs/orleans/deployment/index.md +++ b/docs/orleans/deployment/index.md @@ -1,26 +1,27 @@ --- title: Run an Orleans application description: Learn how to run an Orleans app in .NET. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: overview --- -# Orleans application +# Run an Orleans application -A typical Orleans application consists of a cluster of server processes (silos) where grains live, and a set of client processes, usually web servers, that receive external requests, turn them into grain method calls and return results. Hence, the first thing one needs to do to run an Orleans application is to start a cluster of silos. For testing purposes, a cluster can consist of a single silo. For a reliable production deployment, you'll want more than one silo in a cluster for fault tolerance and scale. +A typical Orleans application consists of a cluster of server processes (silos) where grains live, and a set of client processes (usually web servers) receiving external requests, turning them into grain method calls, and returning results. Therefore, the first step to run an Orleans application is starting a cluster of silos. For testing purposes, a cluster can consist of a single silo. For a reliable production deployment, more than one silo in a cluster is desirable for fault tolerance and scale. -Once the cluster is running, you start one or more client processes that connect to the cluster and can send requests to the grains. Clients connect to a special TCP endpoint on silos — gateway. By default, every silo in a cluster has a client gateway enabled. Clients connect to all silos in parallel for better performance and resilience. +Once the cluster runs, start one or more client processes that connect to the cluster and can send requests to the grains. Clients connect to a special TCP endpoint on silos called a gateway. By default, every silo in a cluster has a client gateway enabled. Clients connect to all silos in parallel for better performance and resilience. ## Configure and start a silo -The silo is configured in conjunction with an . For more information, see [Orleans: Server configuration](../host/configuration-guide/server-configuration.md). After configuring your silo within the host, start the host to initiate the Orleans silo. +Configure the silo in conjunction with an . For more information, see [Orleans: Server configuration](../host/configuration-guide/server-configuration.md). After configuring the silo within the host, start the host to initiate the Orleans silo. -## Configure and connect to a client +## Configure and connect a client -Clients are configured similarly to silos, in that their configuration occurs with `IHost`. For more information, see [Orleans: Client configuration](../host/configuration-guide/client-configuration.md). When the client is configured, start the host instance to have the client connect to silos. +Configure clients similarly to silos, using an `IHost`. For more information, see [Orleans: Client configuration](../host/configuration-guide/client-configuration.md). When the client is configured, start the host instance to have the client connect to the silos. ## Production configurations -The configuration examples we used here are for testing silos and clients running on the same machine as `localhost`. In production, silos and clients usually run on different servers and are configured with one of the reliable cluster configuration options. You can find more about that in the [Configuration guide](../host/configuration-guide/index.md) and in the description of [Cluster management](../implementation/cluster-management.md). +The configuration examples used here are for testing silos and clients running on the same machine (`localhost`). In production, silos and clients usually run on different servers and are configured with one of the reliable cluster configuration options. Find more information about this in the [Configuration guide](../host/configuration-guide/index.md) and the description of [Cluster management](../implementation/cluster-management.md). ## Next steps diff --git a/docs/orleans/deployment/kubernetes.md b/docs/orleans/deployment/kubernetes.md index 10ba92b9e0c84..4bf63fb1881c7 100644 --- a/docs/orleans/deployment/kubernetes.md +++ b/docs/orleans/deployment/kubernetes.md @@ -1,30 +1,32 @@ --- title: Kubernetes hosting description: Learn how to host an Orleans app with Kubernetes. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: how-to +ms.custom: devops --- # Kubernetes hosting -Kubernetes is a popular choice for hosting Orleans applications. Orleans will run in Kubernetes without specific configuration, however, it can also take advantage of extra knowledge which the hosting platform can provide. +Kubernetes is a popular choice for hosting Orleans applications. Orleans runs in Kubernetes without specific configuration; however, it can also take advantage of extra knowledge the hosting platform provides. -The [`Microsoft.Orleans.Hosting.Kubernetes`](https://www.nuget.org/packages/Microsoft.Orleans.Hosting.Kubernetes) package adds integration for hosting an Orleans application in a Kubernetes cluster. The package provides an extension method, , which performs the following actions: +The [`Microsoft.Orleans.Hosting.Kubernetes`](https://www.nuget.org/packages/Microsoft.Orleans.Hosting.Kubernetes) package adds integration for hosting an Orleans application in a Kubernetes cluster. The package provides an extension method, , performing the following actions: -- is set to the pod name. -- is set to the pod IP. -- & are configured to listen on any address, with the configured and . Defaults port values of `11111` and `30000` are used if no values are set explicitly). -- is set to the value of the pod label with the name `orleans/serviceId`. -- is set to the value of the pod label with the name `orleans/clusterId`. -- Early in the startup process, the silo will probe Kubernetes to find which silos do not have corresponding pods and mark those silos as dead. -- The same process will occur at runtime for a subset of all silos, to remove the load on Kubernetes' API server. By default, 2 silos in the cluster will watch Kubernetes. +- Sets to the pod name. +- Sets to the pod IP. +- Configures & to listen on any address, using the configured and . Default port values of `11111` and `30000` are used if values aren't set explicitly. +- Sets to the value of the pod label named `orleans/serviceId`. +- Sets to the value of the pod label named `orleans/clusterId`. +- Early in the startup process, the silo probes Kubernetes to find which silos don't have corresponding pods and marks those silos as dead. +- The same process occurs at runtime for a subset of all silos to reduce the load on Kubernetes' API server. By default, 2 silos in the cluster watch Kubernetes. -Note that the Kubernetes hosting package does not use Kubernetes for clustering. For clustering, a separate clustering provider is still needed. For more information on configuring clustering, see the [Server configuration](../host/configuration-guide/server-configuration.md) documentation. +Note that the Kubernetes hosting package doesn't use Kubernetes for clustering. A separate clustering provider is still needed. For more information on configuring clustering, see the [Server configuration](../host/configuration-guide/server-configuration.md) documentation. -This functionality imposes some requirements on how the service is deployed: +This functionality imposes some requirements on service deployment: -* Silo names must match pod names. -* Pods must have an `orleans/serviceId` and `orleans/clusterId` label which corresponds to the silo's `ServiceId` and `ClusterId`. The above-mentioned method will propagate those labels into the corresponding options in Orleans from environment variables. -* Pods must have the following environment variables set: `POD_NAME`, `POD_NAMESPACE`, `POD_IP`, `ORLEANS_SERVICE_ID`, `ORLEANS_CLUSTER_ID`. +- Silo names must match pod names. +- Pods must have `orleans/serviceId` and `orleans/clusterId` labels corresponding to the silo's `ServiceId` and `ClusterId`. The `UseKubernetesHosting` method propagates these labels into the corresponding Orleans options from environment variables. +- Pods must have the following environment variables set: `POD_NAME`, `POD_NAMESPACE`, `POD_IP`, `ORLEANS_SERVICE_ID`, `ORLEANS_CLUSTER_ID`. The following example shows how to configure these labels and environment variables correctly: @@ -43,17 +45,17 @@ spec: template: metadata: labels: - # This label is used to identify the service to Orleans + # This label identifies the service to Orleans orleans/serviceId: dictionary-app - # This label is used to identify an instance of a cluster to Orleans. - # Typically, this will be the same value as the previous label, or any + # This label identifies an instance of a cluster to Orleans. + # Typically, this is the same value as the previous label, or any # fixed value. - # In cases where you are not using rolling deployments (for example, + # In cases where you don't use rolling deployments (for example, # blue/green deployments), - # this value can allow for distinct clusters which do not communicate - # directly with each others, - # but which still share the same storage and other resources. + # this value can allow for distinct clusters that don't communicate + # directly with each other, + # but still share the same storage and other resources. orleans/clusterId: dictionary-app spec: containers: @@ -61,13 +63,13 @@ spec: image: my-registry.azurecr.io/my-image imagePullPolicy: Always ports: - # Define the ports which Orleans uses + # Define the ports Orleans uses - containerPort: 11111 - containerPort: 30000 env: # The Azure Storage connection string for clustering is injected as an - # environment variable - # It must be created separately using a command such as: + # environment variable. + # You must create it separately using a command such as: # > kubectl create secret generic az-storage-acct ` # --from-file=key=./az-storage-acct.txt - name: STORAGE_CONNECTION_STRING @@ -76,7 +78,7 @@ spec: name: az-storage-acct key: key # Configure settings to let Orleans know which cluster it belongs to - # and which pod it is running in + # and which pod it's running in. - name: ORLEANS_SERVICE_ID valueFrom: fieldRef: @@ -111,7 +113,7 @@ spec: maxSurge: 1 ``` -For RBAC-enabled clusters, the Kubernetes service account for the pods may also need to be granted the required access: +For RBAC-enabled clusters, granting the required access to the Kubernetes service account for the pods might also be necessary: ```yaml kind: Role @@ -139,17 +141,17 @@ roleRef: ## Liveness, readiness, and startup probes -Kubernetes can probe pods to determine the health of a service. For more information, see [Configure liveness, readiness and startup probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) in Kubernetes' documentation. +Kubernetes can probe pods to determine service health. For more information, see [Configure Liveness, Readiness and Startup Probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) in the Kubernetes documentation. -Orleans uses a cluster membership protocol to promptly detect and recover from a process or network failures. Each node monitors a subset of other nodes, sending periodic probes. If a node fails to respond to multiple successive probes from multiple other nodes, then it will be forcibly removed from the cluster. Once a failed node learns that it has been removed, it terminates immediately. Kubernetes will restart the terminated process and it will attempt to rejoin the cluster. +Orleans uses a cluster membership protocol to promptly detect and recover from process or network failures. Each node monitors a subset of other nodes, sending periodic probes. If a node fails to respond to multiple successive probes from multiple other nodes, the cluster forcibly removes it. Once a failed node learns of its removal, it terminates immediately. Kubernetes restarts the terminated process, which then attempts to rejoin the cluster. -Kubernetes' probes can help to determine whether a process in a pod is executing and is not stuck in a zombie state. probes do not verify inter-pod connectivity or responsiveness or perform any application-level functionality checks. If a pod fails to respond to a liveness probe, then Kubernetes may eventually terminate that pod and reschedule it. Kubernetes' probes and Orleans' probes are therefore complementary. +Kubernetes probes help determine whether a process in a pod is executing and isn't stuck in a zombie state. These probes don't verify inter-pod connectivity or responsiveness, nor do they perform application-level functionality checks. If a pod fails to respond to a liveness probe, Kubernetes might eventually terminate that pod and reschedule it. Kubernetes probes and Orleans probes are therefore complementary. -The recommended approach is to configure Liveness Probes in Kubernetes which perform a simple local-only check that the application is performing as intended. These probes serve to terminate the process if there is a total freeze, for example, due to a runtime fault or another unlikely event. +The recommended approach is configuring Liveness Probes in Kubernetes that perform a simple, local-only check that the application performs as intended. These probes serve to terminate the process if there's a total freeze, for example, due to a runtime fault or another unlikely event. ## Resource quotas -Kubernetes works in conjunction with the operating system to implement [resource quotas](https://kubernetes.io/docs/concepts/policy/resource-quotas/). This allows CPU and memory reservations and/or limits to be enforced. For a primary application that is serving interactive load, we recommend not implementing restrictive limits unless necessary. It is important to note that requests and limits are substantially different in their meaning and where they are implemented. Before setting requests or limits, take the time to gain a detailed understanding of how they are implemented and enforced. For example, memory may not be measured uniformly between Kubernetes, the Linux kernel, and your monitoring system. CPU quotas may not be enforced in the way that you expect. +Kubernetes works with the operating system to implement [resource quotas](https://kubernetes.io/docs/concepts/policy/resource-quotas/). This allows enforcing CPU and memory reservations and/or limits. For a primary application serving interactive load, implementing restrictive limits isn't recommended unless necessary. It's important to note that requests and limits differ substantially in meaning and implementation location. Before setting requests or limits, take time to gain a detailed understanding of how they are implemented and enforced. For example, memory might not be measured uniformly between Kubernetes, the Linux kernel, and the monitoring system. CPU quotas might not be enforced as expected. ## Troubleshooting @@ -162,6 +164,5 @@ Unhandled exception. k8s.Exceptions.KubeConfigException: unable to load in-clust at k8s.KubernetesClientConfiguration.InClusterConfig() ``` -* Check that `KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT` environment variables are set inside your Pod. -You can check by executing the following command `kubectl exec -it /bin/bash -c env`. -* Ensure that `automountServiceAccountToken` set to **true** on your Kubernetes `deployment.yaml`. For more information, see [Configure Service Accounts for Pods](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/). +- Check that the `KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT` environment variables are set inside the Pod. Check by executing the command `kubectl exec -it /bin/bash -c env`. +- Ensure `automountServiceAccountToken` is set to `true` in the Kubernetes `deployment.yaml`. For more information, see [Configure Service Accounts for Pods](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/). diff --git a/docs/orleans/deployment/multi-cluster-support/global-single-instance.md b/docs/orleans/deployment/multi-cluster-support/global-single-instance.md index bd125bc0e87b0..5a43ef2fa287f 100644 --- a/docs/orleans/deployment/multi-cluster-support/global-single-instance.md +++ b/docs/orleans/deployment/multi-cluster-support/global-single-instance.md @@ -1,14 +1,15 @@ --- title: Global single-instance grains description: Learn about global single-instance grains and coordination attributes in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Grain coordination attributes -Developers can indicate when and how clusters should coordinate their grain directories concerning a particular grain class. The means we want the same behavior as when running Orleans in a single global cluster: that is, route all calls to a single activation of the grain. Conversely, the `[OneInstancePerCluster]` attribute indicates that each cluster can have its independent activation. This is appropriate if communication between clusters is undesired. +You can indicate when and how clusters should coordinate their grain directories for a particular grain class. The means you want the same behavior as when running Orleans in a single global cluster: route all calls to a single activation of the grain. Conversely, the `[OneInstancePerCluster]` attribute indicates that each cluster can have its independent activation. This is appropriate if you don't want communication between clusters. -The attributes are placed on grain implementations. For example: +Place the attributes on grain implementations. For example: ```csharp using Orleans.MultiCluster; @@ -26,15 +27,15 @@ public class MyLocalGrain : Orleans.Grain, IMyGrain } ``` -If a grain class does not specify either one of those attributes, it defaults to , or if the configuration parameter is set to `true`. +If a grain class doesn't specify either of these attributes, it defaults to . However, it defaults to if you set the configuration parameter to `true`. ## Protocol for global single-instance grains -When a global-single-instance (GSI) grain is accessed, and no activation is known to exist, a special GSI activation protocol is executed before activating a new instance. Specifically, a request is sent to all other clusters in the current [multi-cluster configuration](multi-cluster-configuration.md) to check if they already have activation for this grain. If all responses are negative, a new activation is created in this cluster. Otherwise, the remote activation is used (and a reference to it is cached in the local directory). +When you access a global-single-instance (GSI) grain, and no activation is known to exist, Orleans executes a special GSI activation protocol before activating a new instance. Specifically, it sends a request to all other clusters in the current [multi-cluster configuration](multi-cluster-configuration.md) to check if they already have an activation for this grain. If all responses are negative, Orleans creates a new activation in the current cluster. Otherwise, it uses the remote activation (and caches a reference to it in the local directory). -## Protocol for one instance per cluster grains +## Protocol for one-instance-per-cluster grains -There is no inter-cluster communication for One-Instance-Per-Cluster grains. They simply use the standard Orleans mechanism independently within each cluster. Inside the Orleans framework itself, the following grain classes are marked with the : +There is no inter-cluster communication for one-instance-per-cluster grains. They simply use the standard Orleans mechanism independently within each cluster. Inside the Orleans framework itself, the following grain classes are marked with the : - `ManagementGrain` - `SystemTargetBasedMembershipTable` @@ -42,6 +43,6 @@ There is no inter-cluster communication for One-Instance-Per-Cluster grains. The ## Doubtful activations -If the GSI protocol does not receive conclusive responses from all clusters after 3 retries (or whatever number is specified by the configuration parameter ), it creates a new local "doubtful" activation optimistically, favoring availability over consistency. +If the GSI protocol doesn't receive conclusive responses from all clusters after 3 retries (or the number specified by the configuration parameter), it optimistically creates a new local "doubtful" activation, favoring availability over consistency. -Doubtful activations may be duplicates (because some remote clusters that did not respond during the GSI protocol activation may nevertheless have activation of this grain). Therefore, periodically every 30 seconds (or whatever interval is specified by the configuration parameter ) the GSI protocol is run again for all doubtful activations. This ensures that once communication between clusters is restored, duplicate activations can be detected and removed. +"Doubtful" means the activation might be a duplicate because a remote cluster that didn't respond during the GSI protocol might still have an activation for this grain. Therefore, periodically, every 30 seconds (or the interval specified by the configuration parameter), Orleans runs the GSI protocol again for all doubtful activations. This process ensures that once communication between clusters is restored, the system can detect and remove duplicate activations. diff --git a/docs/orleans/deployment/multi-cluster-support/gossip-channels.md b/docs/orleans/deployment/multi-cluster-support/gossip-channels.md index 791d29bd1a4f4..d81d80e9e6e19 100644 --- a/docs/orleans/deployment/multi-cluster-support/gossip-channels.md +++ b/docs/orleans/deployment/multi-cluster-support/gossip-channels.md @@ -1,55 +1,55 @@ --- title: Multi-cluster communication description: Learn about multi-cluster communication in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Multi-cluster communication -The network must be configured in such a way that any Orleans silo can connect to any other Orleans silo via TCP/IP, regardless of where in the world it is located. Exactly how this is achieved is outside of the scope of Orleans, as it depends on how and where silos are deployed. +You must configure the network so that any Orleans silo can connect to any other Orleans silo via TCP/IP, regardless of where in the world it's located. Exactly how you achieve this is outside the scope of Orleans, as it depends on how and where you deploy the silos. -For example, on Windows Azure, we can use VNets to connect multiple deployments within a region, and gateways to connect VNets across different regions. +For example, on Azure, you can use VNets to connect multiple deployments within a region and gateways to connect VNets across different regions. ## Cluster identifier -Each cluster has its own unique cluster id. The cluster id must be specified in the global configuration. +Each cluster has its own unique cluster ID. You must specify the cluster ID in the global configuration. -Cluster ids may not be empty, nor may they contain commas. Also, if using Azure Table Storage, cluster ids may not contain the characters forbidden for row keys -(/, \, #, ?). +Cluster IDs cannot be empty, nor can they contain commas. Also, if you use Azure Table Storage, cluster IDs cannot contain characters forbidden for row keys (/, \, #, ?). -We recommend using very short strings for the cluster ids, because cluster ids are transmitted frequently and may be stored in storage by some log-view providers. +We recommend using very short strings for cluster IDs because they are transmitted frequently and might be stored in storage by some log-view providers. ## Cluster gateways -Each cluster automatically designates a subset of its active silos to serve as *cluster gateways*. Cluster gateways directly advertise their IP addresses to other clusters, and can thus serve as "points of first contact". By default, at most 10 silos (or whatever number is configured as ) are designated as cluster gateways. +Each cluster automatically designates a subset of its active silos to serve as *cluster gateways*. Cluster gateways directly advertise their IP addresses to other clusters and can thus serve as "points of first contact". By default, Orleans designates at most 10 silos (or the number configured as ) as cluster gateways. -Communication between silos in different clusters does *not* always pass through a gateway. Once a silo has learned and cached the location of a grain activation (no matter in what cluster), it sends messages to that silo directly, even if the silo is not a cluster gateway. +Communication between silos in different clusters does *not* always pass through a gateway. Once a silo learns and caches the location of a grain activation (regardless of the cluster), it sends messages directly to that silo, even if the target silo isn't a cluster gateway. ## Gossip -Gossip is a mechanism for clusters to share configuration and status information. As the name suggests, gossip is decentralized and bidirectional: each silo communicates directly with other silos, both in the same cluster and in other clusters, to exchange information in both directions. +Gossip is a mechanism for clusters to share configuration and status information. As the name suggests, gossip is decentralized and bidirectional: each silo communicates directly with other silos (both in the same cluster and in other clusters) to exchange information in both directions. -**Content**. Gossip contains some or all of the following information: +**Content**: Gossip contains some or all of the following information: - The current time-stamped [multi-cluster configuration](multi-cluster-configuration.md). -- A dictionary that contains information about cluster gateways. The key is the silo address, and the value contains (1) a timestamp, (2) the cluster id, and (3) a status, which is either active or inactive. +- A dictionary containing information about cluster gateways. The key is the silo address, and the value contains (1) a timestamp, (2) the cluster ID, and (3) a status (either active or inactive). -**Fast & Slow Propagation**. When a gateway changes its status, or when an operator injects a new configuration, this gossip information is immediately sent to all silos, clusters, and gossip channels. This happens fast but is not reliable. Should the message be lost due to any reasons (for example, races, broken sockets, silo failures), our periodic background gossip ensures that the information eventually spreads, albeit more slowly. All information is eventually propagated everywhere and is highly resilient to occasional message loss and failures. +**Fast & Slow Propagation**: When a gateway changes its status, or when an operator injects a new configuration, this gossip information is immediately sent to all silos, clusters, and gossip channels. This happens quickly but isn't reliable. If the message is lost for any reason (for example,, races, broken sockets, silo failures), periodic background gossip ensures the information eventually spreads, albeit more slowly. All information eventually propagates everywhere and is highly resilient to occasional message loss and failures. -All gossip data is timestamped, which ensures that newer information replaces older information regardless of the relative timing of messages. For example, newer multi-cluster configurations replace older ones, and newer information about a gateway replaces older information about that gateway. For more details on the representation of gossip data, see the `MultiClusterData` class. It has a `Merge` method that combines gossip data, resolving conflicts using timestamps. +All gossip data is timestamped. This ensures that newer information replaces older information, regardless of the relative timing of messages. For example, newer multi-cluster configurations replace older ones, and newer information about a gateway replaces older information about that gateway. For more details on the representation of gossip data, see the `MultiClusterData` class. It has a `Merge` method that combines gossip data, resolving conflicts using timestamps. ## Gossip channels -When a silo is first started, or when it is restarted after a failure, it needs to have a way to **bootstrap the gossip**. This is the role of the *gossip channel*, which can be configured in the [Silo Configuration](silo-configuration.md). On startup, a silo fetches all the information from the gossip channels. After startup, a silo keeps gossiping periodically, every 30 seconds, or whatever is configured as `BackgroundGossipInterval`. Each time it synchronizes its gossip information with a partner randomly selected from all cluster gateways and gossip channels. +When a silo first starts, or when it restarts after a failure, it needs a way to **bootstrap the gossip**. This is the role of the *gossip channel*, which you can configure in the [Silo Configuration](silo-configuration.md). On startup, a silo fetches all information from the gossip channels. After startup, a silo continues gossiping periodically every 30 seconds (or the interval configured as `BackgroundGossipInterval`). Each time, it synchronizes its gossip information with a partner randomly selected from all cluster gateways and gossip channels. -- Though not strictly required, we recommend always configuring at least two gossip channels, in distinct regions, for better availability. -- Latency of communication with gossip channels is not critical. -- Multiple different services can use the same gossip channel without interference, as long as the ServiceId `Guid` (as specified by their respective configuration) is distinct. -- There is no strict requirement that all silos use the same gossip channels, as long as the channels are sufficient to let a silo initially connect with the "gossiping community" when it starts up. But if a gossip channel is not part of a silo's configuration, and that silo is a gateway, it does not push its status updates to the channel (fast propagation), so it may take longer before those reach the channel via periodic background gossip (slow propagation). +- Although not strictly required, we recommend always configuring at least two gossip channels in distinct regions for better availability. +- Latency of communication with gossip channels isn't critical. +- Multiple different services can use the same gossip channel without interference, as long as the `ServiceId` `Guid` (specified in their respective configurations) is distinct. +- There's no strict requirement that all silos use the same gossip channels, as long as the channels are sufficient to let a silo initially connect with the "gossiping community" when it starts. However, if a gossip channel isn't part of a silo's configuration, and that silo is a gateway, it doesn't push its status updates to that channel (fast propagation). Therefore, it might take longer for those updates to reach the channel via periodic background gossip (slow propagation). -### Azure table-based gossip channel +### Azure Table-based gossip channel -We have already implemented a gossip channel based on Azure Tables. The configuration specifies standard connection strings used for Azure accounts. For example, a configuration could specify two gossip channels with separate Azure storage accounts `usa` and `europe` as follows: +We have implemented a gossip channel based on Azure Tables. The configuration specifies standard connection strings used for Azure accounts. For example, a configuration could specify two gossip channels with separate Azure storage accounts `usa` and `europe` as follows: ```csharp var silo = new HostBuilder() @@ -67,4 +67,4 @@ var silo = new HostBuilder() }) ``` -Multiple different services can use the same gossip channel without interference, as long as the ServiceId `Guid` specified by their respective configuration is distinct. +Multiple different services can use the same gossip channel without interference, as long as the `ServiceId` `Guid` specified by their respective configurations is distinct. diff --git a/docs/orleans/deployment/multi-cluster-support/multi-cluster-configuration.md b/docs/orleans/deployment/multi-cluster-support/multi-cluster-configuration.md index 5e2aed526c040..1d6e2ad045944 100644 --- a/docs/orleans/deployment/multi-cluster-support/multi-cluster-configuration.md +++ b/docs/orleans/deployment/multi-cluster-support/multi-cluster-configuration.md @@ -1,39 +1,40 @@ --- title: Multi-cluster configuration description: Learn about multi-cluster configuration in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Multi-cluster configuration -The multi-cluster configuration determines which clusters are currently part of the multi-cluster. It does not change automatically but is controlled by the operator. Thus, it is quite different from the membership mechanism used within a cluster, which automatically determines the set of silos that are part of the cluster. +The multi-cluster configuration determines which clusters are currently part of the multi-cluster. It doesn't change automatically; instead, the operator controls it. Thus, it's quite different from the membership mechanism used within a cluster, which automatically determines the set of silos that are part of that cluster. -We use the following terminology for the clusters in a service: +We use the following terminology for clusters in a service: - A cluster is *active* if it has at least one active silo, and *inactive* otherwise. - A cluster is *joined* if it is part of the current multi-cluster configuration, and *non-joined* otherwise. Being active/inactive is independent of being joined/non-joined: all four combinations are possible. -All the clusters for a particular service are connected by a [gossip network](gossip-channels.md). The gossip network propagates configuration and status information. +All clusters for a particular service connect via a [gossip network](gossip-channels.md). The gossip network propagates configuration and status information. ## Inject a configuration -An operator issues configuration changes by injecting them into the multi-cluster network. The configurations can be injected into any cluster and spread from there to all active clusters. Each new configuration consists of a list of cluster ids that form the multi-cluster. It also has a UTC timestamp that is used to track its propagation through the gossip network. +An operator issues configuration changes by injecting them into the multi-cluster network. You can inject configurations into any cluster, and they spread from there to all active clusters. Each new configuration consists of a list of cluster IDs that form the multi-cluster. It also has a UTC timestamp used to track its propagation through the gossip network. -Initially, the multi-cluster configuration is null, which means the multi-cluster list is empty (contains no clusters). Thus, the operator *must* initially inject a multi-cluster configuration. Once injected, this configuration persists in all connected silos (while running) and in all specified gossip channels (if those channels are persistent). +Initially, the multi-cluster configuration is null, meaning the multi-cluster list is empty (contains no clusters). Thus, the operator *must* initially inject a multi-cluster configuration. Once injected, this configuration persists in all connected silos (while running) and in all specified gossip channels (if those channels are persistent). -We pose some restrictions on the injection of new configurations that an operator must follow: +We impose some restrictions on injecting new configurations that an operator must follow: - Each new configuration may add several clusters, or remove some clusters (but not both at the same time). -- An operator should not issue a new configuration while a previous configuration change is still being processed. +- An operator should not issue a new configuration while a previous configuration change is still processing. -These restrictions ensure that protocols such as the single-instance protocol can correctly maintain the mutual exclusion of activations even under configuration changes. +These restrictions ensure that protocols like the single-instance protocol can correctly maintain the mutual exclusion of activations, even during configuration changes. ### Management grain -Multi-cluster configurations can be injected on any node in any cluster, using the Orleans Management Grain. -For example, to inject a multi-cluster configuration that consists of the three clusters { us1, eu1, us2 }, we can pass a string enumerable to the management grain: +You can inject multi-cluster configurations on any node in any cluster using the Orleans Management Grain. +For example, to inject a multi-cluster configuration consisting of the three clusters { us1, eu1, us2 }, pass a string enumerable to the management grain: ```csharp var clusters = "us1,eu1,us2".Split(','); @@ -41,13 +42,13 @@ var mgtGrain = client.GetGrain(0); mgtGrain.InjectMultiClusterConfiguration(clusters, "my comment here")); ``` -The first argument to is a collection of cluster ids, which is going to define the new multi-cluster configuration. The second argument is an (optional) comment string that can be used to tag configurations with arbitrary information, such as who injected them and why. +The first argument to is a collection of cluster IDs that defines the new multi-cluster configuration. The second argument is an optional comment string that you can use to tag configurations with arbitrary information, such as who injected them and why. -There is an optional third argument, a boolean called `checkForLaggingSilosFirst`, which defaults to true. It means that the system performs a best-effort check to see if there are any silos anywhere that have not caught up to the current configuration yet, and rejects the change if it finds such a silo. This helps to detect violations of the restriction that only one configuration change should be pending at a time (though it cannot guarantee it under all circumstances). +There's an optional third argument, a boolean named `checkForLaggingSilosFirst`, which defaults to true. It means the system performs a best-effort check to see if any silos anywhere haven't caught up to the current configuration yet. If it finds such a silo, it rejects the change. This helps detect violations of the restriction that only one configuration change should be pending at a time (though it cannot guarantee this under all circumstances). ### Default configuration -In situations where the multi-cluster configuration is known in advance and the deployment is fresh every time (for testing), we may want to supply a default configuration. The global configuration supports an optional attribute which takes a comma-separated list of cluster ids: +In situations where the multi-cluster configuration is known in advance and the deployment is fresh every time (for testing), you might want to supply a default configuration. The global configuration supports an optional attribute which takes a comma-separated list of cluster IDs: ```csharp var silo = new HostBuilder() @@ -61,16 +62,16 @@ var silo = new HostBuilder() .Build(); ``` -After a silo is started with this setting, it checks to see if the current multi-cluster configuration is null, and if so, injects the given configuration with the current UTC timestamp. +After a silo starts with this setting, it checks if the current multi-cluster configuration is null. If it is, the silo injects the given configuration with the current UTC timestamp. > [!WARNING] > Persistent multi-cluster gossip channels (based on AzureTable) retain the last injected configuration unless they are deleted explicitly. In that case, specifying a `DefaultMultiCluster` has no effect when re-deploying a cluster because the configuration stored in the gossip channels is not null.> ### Gossip channel -An operator can also inject the configuration directly into the gossip channel. Changes in the channel are picked up and propagated automatically by the periodic background gossip, though possibly very slowly (using the management grain is much faster). A rough estimate on the propagation time is 30 seconds (or whatever gossip interval is specified in the global configuration) times the binary logarithm of the total number of silos in all clusters. But since the gossip pairs are selected randomly, they can be both much quicker or much slower. +An operator can also inject the configuration directly into the gossip channel. Periodic background gossip automatically picks up and propagates changes in the channel, although possibly very slowly (using the management grain is much faster). A rough estimate of the propagation time is 30 seconds (or the gossip interval specified in the global configuration) times the binary logarithm of the total number of silos across all clusters. However, since gossip pairs are selected randomly, propagation can be much quicker or much slower. -If using the Azure table-based gossip channel, operators can inject a new configuration simply by editing the configuration record in the `OrleansGossipTable`, using some tool for editing data in Azure tables. The configuration record has the following format: +If you use the Azure table-based gossip channel, operators can inject a new configuration simply by editing the configuration record in the `OrleansGossipTable` using a tool for editing Azure table data. The configuration record has the following format: | Name | Type | Value | |-----------------|----------|---------------------------------------------------------| @@ -81,34 +82,34 @@ If using the Azure table-based gossip channel, operators can inject a new config | GossipTimestamp | DateTime | UTC timestamp for the configuration | > [!NOTE] -> When editing this record in storage, the `GossipTimestamp` must also be set to a newer value than it has currently (otherwise the change is ignored). The most convenient and recommended way to do this is to *delete the `GossipTimestamp` field* - our gossip channel implementation then automatically replaces it with a correct, current Timestamp (it uses the Azure Table Timestamp). +> When editing this record in storage, you must also set the `GossipTimestamp` to a newer value than its current value (otherwise, the change is ignored). The most convenient and recommended way to do this is to *delete the `GossipTimestamp` field*. The gossip channel implementation then automatically replaces it with a correct, current timestamp (it uses the Azure Table Timestamp). ## Cluster procedures -Adding or removing a cluster from the multi-cluster often needs to be coordinated within some larger context. We recommend always following the procedures described below when adding/removing clusters from the multi-cluster. +Adding or removing a cluster from the multi-cluster often needs coordination within a larger context. We recommend always following the procedures described below when adding or removing clusters from the multi-cluster. ### Procedure for adding a cluster -1. Start a new Orleans cluster and wait till all silos are up and running. -1. Inject a configuration that contains the new cluster. +1. Start a new Orleans cluster and wait until all silos are up and running. +1. Inject a configuration that includes the new cluster. 1. Start routing user requests to the new cluster. ### Procedure for removing a cluster 1. Stop routing new user requests to the cluster. -1. Inject a configuration that no longer contains the cluster. +1. Inject a configuration that no longer includes the cluster. 1. Stop all silos of the cluster. -Once a cluster has been removed in this way, it can be re-added by following the procedure for adding a new cluster. +Once you remove a cluster this way, you can re-add it by following the procedure for adding a new cluster. ## Activity on non-joined clusters -There can be brief, temporary periods where a cluster is both active and non-joined: +There can be brief, temporary periods when a cluster is both active and non-joined: -- A freshly started cluster may start executing code before it is in the multi-cluster configuration (between steps 1 and 2 of the procedure for adding a cluster) -- A cluster that is being decommissioned may still execute code before the silos are shut down (between steps 2 and 3 of the procedure for removing a cluster). +- A freshly started cluster might start executing code before it's included in the multi-cluster configuration (between steps 1 and 2 of the procedure for adding a cluster). +- A cluster being decommissioned might still execute code before the silos shut down (between steps 2 and 3 of the procedure for removing a cluster). -During those intermediate situations, the following are possible: +During these intermediate situations, the following are possible: -- For global-single-instance grains: A grain may have a duplicate activation on a non-joined cluster. -- For versioned grains: activations on non-joined clusters do not receive notifications when the grain state changes. +- For global-single-instance grains: A grain might have a duplicate activation on a non-joined cluster. +- For versioned grains: Activations on non-joined clusters don't receive notifications when the grain state changes. diff --git a/docs/orleans/deployment/multi-cluster-support/overview.md b/docs/orleans/deployment/multi-cluster-support/overview.md index 1ee903294c039..328960a67d494 100644 --- a/docs/orleans/deployment/multi-cluster-support/overview.md +++ b/docs/orleans/deployment/multi-cluster-support/overview.md @@ -1,32 +1,36 @@ --- title: Multi-cluster support description: Learn about multi-cluster support in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: overview --- # Multi-cluster support -Multi-Cluster support was removed in v2. The documentation below refers to Orleans v1. Orleans v.1.3.0 added support for federating several Orleans clusters into a loosely connected *multi-cluster* that acts as a single service. Multi-clusters facilitate *geo-distribution* as a service, that is, making it easier to run an Orleans application in multiple data centers around the world. Also, a multi-cluster can be run within a single datacenter to get better failure and performance isolation. +> [!IMPORTANT] +> Multi-Cluster support was removed in Orleans v2. The documentation below refers to Orleans v1. + +Orleans v1.3.0 added support for federating several Orleans clusters into a loosely connected *multi-cluster* that acts as a single service. Multi-clusters facilitate *geo-distribution* as a service, making it easier to run an Orleans application in multiple data centers around the world. You can also run a multi-cluster within a single datacenter to get better failure and performance isolation. All mechanisms are designed with particular attention to: 1. Minimize communication between clusters. -1. Let each cluster run autonomously even if other clusters fail or become unreachable. +1. Let each cluster run autonomously, even if other clusters fail or become unreachable. ## Configuration and operation -Below we document how to configure and operate a multi-cluster. +Below, we document how to configure and operate a multi-cluster. -[**Communication**](gossip-channels.md). Clusters communicate via the same silo-to-silo connections that are used within a cluster. To exchange status and configuration information, Clusters use a gossip mechanism and gossip channel implementations. +[**Communication**](gossip-channels.md): Clusters communicate via the same silo-to-silo connections used within a cluster. To exchange status and configuration information, clusters use a gossip mechanism and gossip channel implementations. -[**Silo Configuration**](silo-configuration.md). Silos need to be configured so they know which cluster they belong to (each cluster is identified by a unique string). Also, each silo needs to be configured with connection strings that allow them to connect to one or more [gossip channels](gossip-channels.md) on startup. +[**Silo Configuration**](silo-configuration.md): You need to configure silos so they know which cluster they belong to (each cluster is identified by a unique string). Also, you need to configure each silo with connection strings that allow it to connect to one or more [gossip channels](gossip-channels.md) on startup. -[**Multi-Cluster Configuration Injection**](multi-cluster-configuration.md). At runtime, the service operator can specify and/or change the *multi-cluster configuration*, which contains a list of cluster ids, to specify which clusters are part of the current multi-cluster. This is done by calling the management grain in any one of the clusters. +[**Multi-Cluster Configuration Injection**](multi-cluster-configuration.md): At runtime, the service operator can specify and/or change the *multi-cluster configuration*, which contains a list of cluster IDs, to specify which clusters are part of the current multi-cluster. You do this by calling the management grain in any one of the clusters. ## Multi-cluster grains -Below we document how to use multi-cluster functionality at the application level. +Below, we document how to use multi-cluster functionality at the application level. -[**Global-Single-Instance Grains**](global-single-instance.md). Developers can indicate when and how clusters should coordinate their grain directories concerning a particular grain class. The means we want the same behavior as when running Orleans in a single global cluster: that is, route all calls to a single activation of the grain. Conversely, the indicates that each cluster can have its independent activation. This is appropriate if communication between clusters is undesired. +[**Global-Single-Instance Grains**](global-single-instance.md): You can indicate when and how clusters should coordinate their grain directories for a particular grain class. The means you want the same behavior as when running Orleans in a single global cluster: route all calls to a single activation of the grain. Conversely, the indicates that each cluster can have its independent activation. This is appropriate if you don't want communication between clusters. -**Log-view grains** *(not in v.1.3.0)*. A special type of grain that uses a new API, similar to event sourcing, for synchronizing or persisting grain state. It can be used to automatically and efficiently synchronize the state of a grain between clusters and with storage. Because its synchronization algorithms are safe to use with reentrant grains, and are optimized to use batching and replication, it can perform better than standard grains when a grain is frequently accessed in multiple clusters, and/or when it is written to storage frequently. Support for log-view grains is not part of the main branch yet. We have a prerelease including samples and a bit of documentation in the [`geo-orleans` branch](https://github.com/sebastianburckhardt/orleans/tree/geo-samples). It is currently being evaluated in production by an early adopter. +**Log-view grains** *(not in v1.3.0)*: A special type of grain that uses a new API, similar to event sourcing, for synchronizing or persisting grain state. You can use it to automatically and efficiently synchronize the state of a grain between clusters and with storage. Because its synchronization algorithms are safe for use with reentrant grains and are optimized for batching and replication, it can perform better than standard grains when a grain is frequently accessed in multiple clusters and/or written to storage frequently. Support for log-view grains isn't part of the main branch yet. We have a prerelease including samples and some documentation in the [`geo-orleans` branch](https://github.com/sebastianburckhardt/orleans/tree/geo-samples). It's currently being evaluated in production by an early adopter. diff --git a/docs/orleans/deployment/multi-cluster-support/silo-configuration.md b/docs/orleans/deployment/multi-cluster-support/silo-configuration.md index 9a6834141f0f1..403483ea262ab 100644 --- a/docs/orleans/deployment/multi-cluster-support/silo-configuration.md +++ b/docs/orleans/deployment/multi-cluster-support/silo-configuration.md @@ -1,12 +1,13 @@ --- title: Silo configuration -description: Learn about silo configuration in .NET Orleans. -ms.date: 07/03/2024 +description: Learn about silo configuration for multi-cluster support in .NET Orleans. +ms.date: 05/23/2025 +ms.topic: conceptual --- # Orleans silo configuration -To get a quick overview, we show all relevant configuration parameters (including optional ones) in XML syntax below: +For a quick overview, we show all relevant configuration parameters (including optional ones) in XML syntax below: ```xml @@ -56,20 +57,20 @@ var silo = new HostBuilder() }); ``` -As usual, all configuration settings can also be read and written programmatically, via the respective members of the class. +As usual, you can also read and write all configuration settings programmatically via the respective members of the class. The is an arbitrary ID for identifying this service. It must be the same for all clusters and all silos. -The section is optional—if not present, all multi-cluster support is disabled for this silo. +The `MultiClusterNetwork` section is optional. If it's not present, all multi-cluster support is disabled for this silo. -The **required parameters** and are explained in the section on [Multi-Cluster Communication](gossip-channels.md). +The **required parameters** and are explained in [Multi-cluster communication](gossip-channels.md). -The optional parameters and are explained in the section on [Multi-Cluster Communication](gossip-channels.md). +The optional parameters and are explained in [Multi-cluster communication](gossip-channels.md). -The optional parameter is explained in the section on [Multi-Cluster Configuration](multi-cluster-configuration.md). +The optional parameter is explained in [Multi-cluster configuration](multi-cluster-configuration.md). -The optional parameters , , and are explained in the section on [Global-Single-Instance Grains](global-single-instance.md). +The optional parameters , , and are explained in [Global single-instance grains](global-single-instance.md). ## Orleans client configuration -No extra configuration is required for Orleans client. The same client may not connect to silos in different clusters (the silo refuses the connection in that situation). +No extra configuration is required for the Orleans client. The same client cannot connect to silos in different clusters (the silo refuses the connection in that situation). diff --git a/docs/orleans/deployment/service-fabric.md b/docs/orleans/deployment/service-fabric.md index 8903836a653fb..1d38158b80aff 100644 --- a/docs/orleans/deployment/service-fabric.md +++ b/docs/orleans/deployment/service-fabric.md @@ -1,40 +1,42 @@ --- title: Host with Service Fabric description: Learn how to host an Orleans app with Service Fabric. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: how-to +ms.custom: devops --- # Host with Service Fabric -Orleans can be hosted on [Azure Service Fabric](/azure/service-fabric) using the [Microsoft.ServiceFabric.Services](https://www.nuget.org/packages/Microsoft.ServiceFabric.Services) and [Microsoft.Orleans.Server](https://www.nuget.org/packages/Microsoft.Orleans.Server) NuGet packages. Silos should be hosted as unpartitioned, stateless services since Orleans manages the distribution of grains itself. Other hosting options, such as partitioned and stateful, are more complex and yield no benefits without additional customization on the part of the developer. It's recommended to host Orleans unpartitioned and stateless. +Host Orleans on [Azure Service Fabric](/azure/service-fabric) using the [Microsoft.ServiceFabric.Services](https://www.nuget.org/packages/Microsoft.ServiceFabric.Services) and [Microsoft.Orleans.Server](https://www.nuget.org/packages/Microsoft.Orleans.Server) NuGet packages. Host silos as unpartitioned, stateless services because Orleans manages grain distribution itself. Other hosting options, such as partitioned and stateful, are more complex and yield no benefits without additional customization. Hosting Orleans as unpartitioned and stateless is recommended. -## Service Fabric stateless service as a Silo +## Service Fabric stateless service as a silo -Whether you're creating a new Service Fabric Application or adding Orleans to an existing one, you'll need both the `Microsoft.ServiceFabric.Services` and `Microsoft.Orleans.Server` package references in your project. The stateless service project needs an implementation on the and a subclass of the . +Whether creating a new Service Fabric Application or adding Orleans to an existing one, both the `Microsoft.ServiceFabric.Services` and `Microsoft.Orleans.Server` package references are needed in the project. The stateless service project requires an implementation of and a subclass of . The Silo lifecycle follows the typical communication listener lifecycle: -- It's initialized with . -- It's gracefully terminated with . -- Or it's abruptly terminated with . +- Initialized with . +- Gracefully terminated with . +- Or abruptly terminated with . -Since Orleans Silos are capable of living within the confines of the , the implementation of the `ICommunicationListener` is a wrapper around the `IHost`. The `IHost` is initialized in the `OpenAsync` method and gracefully terminated in the `CloseAsync` method: +Since Orleans Silos can live within the confines of the , the implementation of `ICommunicationListener` is a wrapper around the `IHost`. The `IHost` initializes in the `OpenAsync` method and gracefully terminates in the `CloseAsync` method: -| `ICommunicationListener` | `IHost` interactions | -|---------|---------| -| | The `IHost` instance is created and a call to is made. | -| | A call to on the host instance is awaited. | -| | A call to is forcefully evaluated, with `GetAwaiter().GetResult()`. | +| `ICommunicationListener` | `IHost` interactions | +| -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | +| | The `IHost` instance is created and a call to is made. | +| | A call to on the host instance is awaited. | +| | A call to is forcefully evaluated with `GetAwaiter().GetResult()`. | ## Cluster support -Official clustering support is available from various packages including: +Official clustering support is available from various packages, including: -* [Microsoft.Orleans.Clustering.AzureStorage](https://www.nuget.org/packages/Microsoft.Orleans.Clustering.AzureStorage) -* [Microsoft.Orleans.Clustering.AdoNet](https://www.nuget.org/packages/Microsoft.Orleans.Clustering.AdoNet) -* [Microsoft.Orleans.Clustering.DynamoDB](https://www.nuget.org/packages/Microsoft.Orleans.Clustering.DynamoDB) +- [Microsoft.Orleans.Clustering.AzureStorage](https://www.nuget.org/packages/Microsoft.Orleans.Clustering.AzureStorage) +- [Microsoft.Orleans.Clustering.AdoNet](https://www.nuget.org/packages/Microsoft.Orleans.Clustering.AdoNet) +- [Microsoft.Orleans.Clustering.DynamoDB](https://www.nuget.org/packages/Microsoft.Orleans.Clustering.DynamoDB) -There are also several third-party packages available for other services such as CosmosDB, Kubernetes, Redis, and Aerospike. For more information, see [Cluster management in Orleans](../implementation/cluster-management.md). +Several third-party packages are also available for other services such as Cosmos DB, Kubernetes, Redis, and Aerospike. For more information, see [Cluster management in Orleans](../implementation/cluster-management.md). ## Example project @@ -42,19 +44,19 @@ In the stateless service project, implement the `ICommunicationListener` interfa :::code source="snippets/service-fabric/stateless/HostedServiceCommunicationListener.cs"::: -The `HostedServiceCommunicationListener` class accepts a `Func> createHost` constructor parameter. This is later used to create the `IHost` instance in the `OpenAsync` method. +The `HostedServiceCommunicationListener` class accepts a `Func> createHost` constructor parameter. This function is later used to create the `IHost` instance in the `OpenAsync` method. -The next part of the stateless service project is to implement the `StatelessService` class. The following example shows the subclass of the `StatelessService` class: +The next part of the stateless service project is implementing the `StatelessService` class. The following example shows the subclass of `StatelessService`: :::code source="snippets/service-fabric/stateless/OrleansHostedStatelessService.cs"::: -In the preceding example, the `OrleansHostedStatelessService` class is responsible for yielding an `ICommunicationListener` instance. The `CreateServiceInstanceListeners` method is called by the Service Fabric runtime when the service is initialized. +In the preceding example, the `OrleansHostedStatelessService` class is responsible for yielding an `ICommunicationListener` instance. The Service Fabric runtime calls the `CreateServiceInstanceListeners` method when the service initializes. -Pulling these two classes together, the following example shows the complete stateless service project _Program.cs_ file: +Pulling these two classes together, the following example shows the complete _Program.cs_ file for the stateless service project: :::code source="snippets/service-fabric/stateless/Program.cs"::: -In the preceding code: +In the preceding code: - The method registers the `OrleansHostedStatelessService` class with the Service Fabric runtime. -- The `CreateHostAsync` delegate is used to create the `IHost` instance. +- The `CreateHostAsync` delegate creates the `IHost` instance. diff --git a/docs/orleans/deployment/troubleshooting-azure-cloud-services-deployments.md b/docs/orleans/deployment/troubleshooting-azure-cloud-services-deployments.md index d1b22d64aef18..258732abeb468 100644 --- a/docs/orleans/deployment/troubleshooting-azure-cloud-services-deployments.md +++ b/docs/orleans/deployment/troubleshooting-azure-cloud-services-deployments.md @@ -1,57 +1,53 @@ --- title: Troubleshoot Azure Cloud Service deployments -description: Learn how to troubleshoot an Orleans app with Azure Cloud Service deployment. -ms.date: 07/03/2024 +description: Learn how to troubleshoot an Orleans app deployed to Azure Cloud Services. +ms.date: 05/23/2025 +ms.topic: troubleshooting +ms.custom: devops --- # Troubleshoot Azure Cloud Service deployments -This page gives some general guidelines for troubleshooting any issues that occur while deploying to Azure Cloud Services. -These are very common issues to watch out for. Be sure to check the logs for more information. +This page provides general guidelines for troubleshooting issues occurring when deploying to Azure Cloud Services. These are common issues to watch out for. Check the logs for more detailed information. ## The `SiloUnavailableException` -First, check to make sure that you are starting the silos before attempting to initialize the client. Sometimes the -silos take a long time to start so it can be beneficial to try to initialize the client multiple times. If it still throws an -exception, then there might be another issue with the silos. +First, ensure silos start before attempting to initialize the client. Sometimes silos take a long time to start, so trying to initialize the client multiple times can be beneficial. If it still throws an exception, another issue might exist with the silos. -Check the silo configuration and make sure that the silos are starting up properly. +Check the silo configuration and ensure the silos start properly. ## Common connection string issues -- Using the local connection string when deploying to Azure – the website will fail to connect. -- Using different connection strings for the silos and the front end (web and worker roles) – the website will fail to -initialize the client because it cannot connect to the silos. +- **Using the local connection string when deploying to Azure**: The website fails to connect. +- **Using different connection strings for silos and the front end (web and worker roles)**: The website fails to initialize the client because it cannot connect to the silos. -The connection string configuration can be checked in the Azure Portal. The logs may not display properly if the connection -strings are not set up correctly. +Check the connection string configuration in the Azure Portal. Logs might not display properly if connection strings aren't set up correctly. -## Modify the configuration files improperly +## Improperly modified configuration files -Make sure that the proper endpoints are configured in the _ServiceDefinition.csdef_ file or else the deployment will not work. It will give errors saying that it cannot get the endpoint information. +Ensure proper endpoints are configured in the _ServiceDefinition.csdef_ file; otherwise, the deployment won't work. Errors stating that endpoint information cannot be obtained will occur. ## Missing logs -Make sure that the connection strings are set up properly. +Ensure the connection strings are set up properly. -It is likely that the _Web.config_ file in the web role or the _app.config_ file in the worker role was modified improperly. Incorrect versions in these files can cause issues with the deployment. Be careful when dealing with updates. +It's likely the _Web.config_ file in the web role or the _app.config_ file in the worker role was modified improperly. Incorrect versions in these files can cause deployment issues. Be careful when handling updates. ## Version issues -Make sure that the same version of Orleans is used in every project in the solution. Not doing this can lead to the worker -role recycling. Check the logs for more information. Visual Studio provides some silo startup error messages in the deployment history. +Ensure the same version of Orleans is used in every project in the solution. Using different versions can lead to worker role recycling. Check the logs for more information. Visual Studio provides some silo startup error messages in the deployment history. ## Role keeps recycling -- Check that all the appropriate Orleans assemblies are in the solution and have Copy Local set to True. -- Check the logs to see if there is an unhandled exception while initializing. -- Make sure that the connection strings are correct. -- Check the Azure Cloud Services troubleshooting pages for more information. +- Verify all appropriate Orleans assemblies are in the solution and have **Copy Local** set to **True**. +- Check the logs for unhandled exceptions during initialization. +- Ensure the connection strings are correct. +- Refer to the Azure Cloud Services troubleshooting pages for more information. ## How to check logs -- Use the Cloud Explorer in Visual Studio to navigate to the appropriate storage table or blob in the storage account. The WADLogsTable is a good starting point for looking at the logs. -- You might only be logging errors. If you want informational logs as well, you will need to modify the configuration to set the logging severity level. +- Use Cloud Explorer in Visual Studio to navigate to the appropriate storage table or blob in the storage account. The `WADLogsTable` is a good starting point for examining logs. +- Only errors might be logged. If informational logs are also desired, modify the configuration to set the logging severity level. Programmatic configuration: @@ -60,17 +56,17 @@ Programmatic configuration: Declarative configuration: -- Add `` to the _OrleansConfiguration.xml_ and/or the _ClientConfiguration.xml_ files. +- Add `` to the _OrleansConfiguration.xml_ and/or _ClientConfiguration.xml_ files. -In the _diagnostics.wadcfgx_ file for the web and worker roles, make sure to set the `scheduledTransferLogLevelFilter` attribute in the `Logs` element to `Information`, as this is an additional layer of trace filtering that defines which traces are sent to the `WADLogsTable` in Azure Storage. +In the _diagnostics.wadcfgx_ file for the web and worker roles, ensure the `scheduledTransferLogLevelFilter` attribute in the `Logs` element is set to `Information`. This setting acts as an additional layer of trace filtering defining which traces are sent to the `WADLogsTable` in Azure Storage. -You can find more information about this in the configuration guide. +Find more information about this in the configuration guide. ## Compatibility with ASP.NET -The razor view engine included in ASP.NET uses the same code generation assemblies as Orleans (`Microsoft.CodeAnalysis` and `Microsoft.CodeAnalysis.CSharp`). This can present a version compatibility problem at runtime. +The Razor view engine included in ASP.NET uses the same code generation assemblies as Orleans (`Microsoft.CodeAnalysis` and `Microsoft.CodeAnalysis.CSharp`). This can present a version compatibility problem at runtime. -To resolve this, try upgrading `Microsoft.CodeDom.Providers.DotNetCompilerPlatform` (this is the NuGet package ASP.NET uses to include the above assemblies) to the latest version, and setting binding redirects like this: +To resolve this, try upgrading `Microsoft.CodeDom.Providers.DotNetCompilerPlatform` (the NuGet package ASP.NET uses to include these assemblies) to the latest version and setting binding redirects like this: ```xml diff --git a/docs/orleans/deployment/troubleshooting-deployments.md b/docs/orleans/deployment/troubleshooting-deployments.md index 0810b72b057a4..e4ef990ee92db 100644 --- a/docs/orleans/deployment/troubleshooting-deployments.md +++ b/docs/orleans/deployment/troubleshooting-deployments.md @@ -1,58 +1,56 @@ --- title: Troubleshoot deployments -description: Learn how to troubleshoot an Orleans app deployment. -ms.date: 07/03/2024 +description: Learn how to troubleshoot common Orleans deployment issues. +ms.date: 05/23/2025 +ms.topic: troubleshooting +ms.custom: devops --- # Troubleshoot deployments -This page gives some general guidelines for troubleshooting any issues that occur while deploying to Azure Cloud Services. -These are very common issues to watch out for. -Be sure to check the logs for more information. +This page provides general guidelines for troubleshooting issues occurring during deployment, particularly with Azure Cloud Services. These are common issues to watch out for. Check the logs for more detailed information. ## The `SiloUnavailableException` -First, check to make sure that you are starting the silos before attempting to initialize the client. Sometimes the silos take a long time to start so it can be beneficial to try to initialize the client multiple times. If it still throws an exception, then there might be another issue with the silos. +First, ensure silos start before attempting to initialize the client. Sometimes silos take a long time to start, so trying to initialize the client multiple times can be beneficial. If it still throws an exception, another issue might exist with the silos. -Check the silo configuration and make sure that the silos are starting up properly. +Check the silo configuration and ensure the silos start properly. ## Common connection string issues -- Using the local connection string when deploying to Azure – the website will fail to connect. -- Using different connection strings for the silos and the front end (web and worker roles) – the website will fail to initialize the client because it cannot connect to the silos. +- **Using the local connection string when deploying to Azure**: The website fails to connect. +- **Using different connection strings for silos and the front end (web and worker roles)**: The website fails to initialize the client because it cannot connect to the silos. -The connection string configuration can be checked in the Azure Portal. The logs may not display properly if the connection -strings are not set up correctly. +Check the connection string configuration in the Azure Portal. Logs might not display properly if connection strings aren't set up correctly. -## Modify the configuration files improperly +## Improperly modified configuration files -Make sure that the proper endpoints are configured in the _ServiceDefinition.csdef_ file or else the deployment will not work. -It will give errors saying that it cannot get the endpoint information. +Ensure proper endpoints are configured in the _ServiceDefinition.csdef_ file; otherwise, the deployment won't work. Errors stating that endpoint information cannot be obtained will occur. ## Missing logs -Make sure that the connection strings are set up properly. +Ensure the connection strings are set up properly. -Likely, the _Web.config_ file in the web role or the _app.config_ file in the worker role was modified improperly. Incorrect versions in these files can cause issues with the deployment. Be careful when dealing with updates. +It's likely the _Web.config_ file in the web role or the _app.config_ file in the worker role was modified improperly. Incorrect versions in these files can cause deployment issues. Be careful when handling updates. ## Version issues -Make sure that the same version of Orleans is used in every project in the solution. Not doing this can lead to the worker role recycling. Check the logs for more information. Visual Studio provides some silo startup error messages in the deployment history. +Ensure the same version of Orleans is used in every project in the solution. Using different versions can lead to worker role recycling. Check the logs for more information. Visual Studio provides some silo startup error messages in the deployment history. ## Role keeps recycling -- Check that all the appropriate Orleans assemblies are in the solution and have `Copy Local` set to `True`. -- Check the logs to see if there is an unhandled exception while initializing. -- Make sure that the connection strings are correct. -- Check the Azure Cloud Services troubleshooting pages for more information. +- Verify all appropriate Orleans assemblies are in the solution and have **Copy Local** set to **True**. +- Check the logs for unhandled exceptions during initialization. +- Ensure the connection strings are correct. +- Refer to the Azure Cloud Services troubleshooting pages for more information. ## How to check logs -- Use the cloud explorer in Visual Studio to navigate to the appropriate storage table or blob in the storage account. +- Use Cloud Explorer in Visual Studio to navigate to the appropriate storage table or blob in the storage account. -The WADLogsTable is a good starting point for looking at the logs. +The `WADLogsTable` is a good starting point for examining logs. -- You might only be logging errors. If you want informational logs as well, you will need to modify the configuration to set the logging severity level. +- Only errors might be logged. If informational logs are also desired, modify the configuration to set the logging severity level. Programmatic configuration: @@ -61,17 +59,17 @@ Programmatic configuration: Declarative configuration: -- Add `` to the _OrleansConfiguration.xml_ and/or the _ClientConfiguration.xml_ files. +- Add `` to the _OrleansConfiguration.xml_ and/or _ClientConfiguration.xml_ files. -In the _diagnostics.wadcfgx_ file for the web and worker roles, make sure to set the `scheduledTransferLogLevelFilter` attribute in the `Logs` element to `Information`, as this is an additional layer of trace filtering that defines which traces are sent to the `WADLogsTable` in Azure Storage. +In the _diagnostics.wadcfgx_ file for the web and worker roles, ensure the `scheduledTransferLogLevelFilter` attribute in the `Logs` element is set to `Information`. This setting acts as an additional layer of trace filtering defining which traces are sent to the `WADLogsTable` in Azure Storage. -You can find more information about this in the [Configuration Guide](../host/configuration-guide/index.md). +Find more information about this in the [Configuration Guide](../host/configuration-guide/index.md). ## Compatibility with ASP.NET -The razor view engine included in ASP.NET uses the same code generation assemblies as Orleans (`Microsoft.CodeAnalysis` and `Microsoft.CodeAnalysis.CSharp`). This can present a version compatibility problem at runtime. +The Razor view engine included in ASP.NET uses the same code generation assemblies as Orleans (`Microsoft.CodeAnalysis` and `Microsoft.CodeAnalysis.CSharp`). This can present a version compatibility problem at runtime. -To resolve this, try upgrading `Microsoft.CodeDom.Providers.DotNetCompilerPlatform` (this is the NuGet package ASP.NET uses to include the above assemblies) to the latest version, and setting binding redirects like this: +To resolve this, try upgrading `Microsoft.CodeDom.Providers.DotNetCompilerPlatform` (the NuGet package ASP.NET uses to include these assemblies) to the latest version and setting binding redirects like this: ```xml diff --git a/docs/orleans/grains/cancellation-tokens.md b/docs/orleans/grains/cancellation-tokens.md index 257d353d0e9de..16fefbe58909f 100644 --- a/docs/orleans/grains/cancellation-tokens.md +++ b/docs/orleans/grains/cancellation-tokens.md @@ -1,34 +1,35 @@ --- title: Grain cancellation tokens description: Learn how to use grain cancellation tokens in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 03/31/2025 +ms.topic: conceptual --- # Grain cancellation tokens -The Orleans runtime provides a mechanism called grain cancellation token, that enables the developer to cancel an executing grain operation. +The Orleans runtime provides a mechanism called a grain cancellation token, enabling cancellation of an executing grain operation. ## Description - is a wrapper around the standard , which enables cooperative cancellation between threads, thread pool work items, or `Task` objects, and can be passed as a grain method argument. + is a wrapper around the standard . It enables cooperative cancellation between threads, thread pool work items, or `Task` objects, and can be passed as a grain method argument. -A is an object that provides a cancellation token through its Token property and sends a cancellation message by calling its method. +A provides a cancellation token through its `Token` property and sends a cancellation message when its method is called. ## Usage -* Instantiate a `CancellationTokenSource` object, which manages and sends cancellation notifications to the individual cancellation tokens. +- Instantiate a `CancellationTokenSource` object. This object manages and sends cancellation notifications to individual cancellation tokens. ```csharp var tcs = new GrainCancellationTokenSource(); ``` -* Pass the token returned by the property to each grain method that listens for cancellation. +- Pass the token returned by the property to each grain method listening for cancellation. ```csharp var waitTask = grain.LongIoWork(tcs.Token, TimeSpan.FromSeconds(10)); ``` -* A cancellable grain operation needs to handle the underlying `CancellationToken` property of `GrainCancellationToken` just like it would do in any other .NET code. +- A cancellable grain operation needs to handle the underlying `CancellationToken` property of `GrainCancellationToken` just like in any other .NET code. ```csharp public async Task LongIoWork(GrainCancellationToken tc, TimeSpan delay) @@ -40,13 +41,13 @@ public async Task LongIoWork(GrainCancellationToken tc, TimeSpan delay) } ``` -* Call the `GrainCancellationTokenSource.Cancel` method to initiate cancellation. +- Call the `GrainCancellationTokenSource.Cancel` method to initiate cancellation. ```csharp await tcs.Cancel(); ``` -* Call the `Dispose` method when you are finished with the `GrainCancellationTokenSource` object. +- Call the `Dispose` method when finished with the `GrainCancellationTokenSource` object. ```csharp tcs.Dispose(); @@ -54,6 +55,6 @@ tcs.Dispose(); #### Important considerations -* The `GrainCancellationTokenSource.Cancel` method returns a `Task`, and to ensure cancellation the cancel call must be retried in case of transient communication failure. -* Callbacks registered in underlying `System.Threading.CancellationToken` are subjects to single-threaded execution guarantees within the grain activation on which they were registered. -* Each `GrainCancellationToken` can be passed through multiple methods invocations. +- The `GrainCancellationTokenSource.Cancel` method returns a `Task`. To ensure cancellation, retry the cancel call in case of transient communication failure. +- Callbacks registered on the underlying `System.Threading.CancellationToken` are subject to single-threaded execution guarantees within the grain activation where they were registered. +- Each `GrainCancellationToken` can be passed through multiple method invocations. diff --git a/docs/orleans/grains/code-generation.md b/docs/orleans/grains/code-generation.md index 841d72ac3a68f..a1ffacc329466 100644 --- a/docs/orleans/grains/code-generation.md +++ b/docs/orleans/grains/code-generation.md @@ -1,13 +1,14 @@ --- title: Code generation description: Learn how to use code generation in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual zone_pivot_groups: orleans-version --- # Orleans code generation -Before Orleans 7.0, source generation was much more manual and required explicit developer intervention. Starting with Orleans 7.0, code generation is automatic and requires no developer intervention. However, there are still cases where developers may want to influence code generation, for example, to generate code for types that are not automatically generated or to generate code for types in another assembly. +Before Orleans 7.0, source generation was more manual and required explicit developer intervention. Starting with Orleans 7.0, code generation is automatic and typically requires no intervention. However, cases still exist where influencing code generation might be desired, for example, to generate code for types not automatically generated or for types in another assembly. ## Enable code generation @@ -15,13 +16,13 @@ Before Orleans 7.0, source generation was much more manual and required explicit :::zone target="docs" pivot="orleans-7-0" -Orleans generates C# source code for your app at build time. All projects, including your host, need to have the appropriate NuGet packages installed to enable code generation. The following packages are available: +Orleans generates C# source code for the app at build time. All projects, including the host, need the appropriate NuGet packages installed to enable code generation. The following packages are available: - All clients should reference [Microsoft.Orleans.Client](https://nuget.org/packages/Microsoft.Orleans.Client). - All silos (servers) should reference [Microsoft.Orleans.Server](https://nuget.org/packages/Microsoft.Orleans.Server). - All other packages should reference [Microsoft.Orleans.Sdk](https://nuget.org/packages/Microsoft.Orleans.Sdk). -Use the to specify that the type is intended to be serialized and that serialization code should be generated for the type. For more information, see [Use Orleans serialization](../host/configuration-guide/serialization.md#use-orleans-serialization). +Use the to specify that the type is intended for serialization and that Orleans should generate serialization code for it. For more information, see [Use Orleans serialization](../host/configuration-guide/serialization.md#use-orleans-serialization). :::zone-end @@ -29,24 +30,24 @@ Use the to specify that the type is i :::zone target="docs" pivot="orleans-3-x" -The Orleans runtime makes use of generated code to ensure proper serialization of types that are used across the cluster as well as for generating boilerplate, which abstracts away the implementation details of method shipping, exception propagation, and other internal runtime concepts. Code generation can be performed either when your projects are being built or when your application initializes. +The Orleans runtime uses generated code to ensure proper serialization of types used across the cluster and to generate boilerplate code. This boilerplate abstracts away implementation details of method dispatching, exception propagation, and other internal runtime concepts. Code generation can be performed either when building projects or when the application initializes. :::zone-end -### What happens during build? +### Build-time code generation :::zone target="docs" pivot="orleans-7-0" -At build time, Orleans generates code for all types that are marked with . If a type isn't marked with `GenerateSerializer`, it will not be serialized by Orleans. +At build time, Orleans generates code for all types marked with . If a type isn't marked with `GenerateSerializer`, Orleans won't serialize it. -If you're developing with F# or Visual Basic, you can also use code generation. For more information, see the following samples: +If developing with F# or Visual Basic, code generation can also be used. For more information, see these samples: - [Orleans F# sample app](/samples/dotnet/samples/orleans-fsharp-sample) - [Orleans Visual Basic sample app](/samples/dotnet/samples/orleans-vb-sample) -These examples demonstrate how to consume the , specifying types in the assembly which the source generator should inspect and generate source for. +These examples demonstrate using the , specifying types in the assembly for the source generator to inspect and generate source code. :::zone-end @@ -54,26 +55,26 @@ These examples demonstrate how to consume the -The preferred method for performing code generation is at build time. Build time code generation could be enabled by using one of the following packages: +The preferred method for code generation is at build time. Enable build-time code generation using one of the following packages: -+ [Microsoft.Orleans.OrleansCodeGenerator.Build](https://www.nuget.org/packages/Microsoft.Orleans.OrleansCodeGenerator.Build/). A package that uses Roslyn for code generation and uses .NET Reflection for analysis. -+ [Microsoft.Orleans.CodeGenerator.MSBuild](https://www.nuget.org/packages/Microsoft.Orleans.CodeGenerator.MSBuild/). A new code generation package that leverages Roslyn both for code generation and code analysis. It does not load application binaries, and as a result, avoids issues caused by clashing dependency versions and differing target frameworks. The new code generator also improves support for incremental builds, which should result in shorter build times. +- `Microsoft.Orleans.OrleansCodeGenerator.Build`: A package using Roslyn for code generation and .NET Reflection for analysis. +- `Microsoft.Orleans.CodeGenerator.MSBuild`: A newer code generation package leveraging Roslyn for both code generation and analysis. It doesn't load application binaries, avoiding issues caused by clashing dependency versions and differing target frameworks. This code generator also improves support for incremental builds, resulting in shorter build times. -One of these packages should be installed into all projects which contain grains, grain interfaces, custom serializers, or types that are sent between grains. Installing a package injects a target into the project which will generate code at build time. +Install one of these packages into all projects containing grains, grain interfaces, custom serializers, or types sent between grains. Installing a package injects a target into the project that generates code at build time. -Both packages (`Microsoft.Orleans.CodeGenerator.MSBuild` and `Microsoft.Orleans.OrleansCodeGenerator.Build`) only support C# projects. Other languages are supported either using the `Microsoft.Orleans.OrleansCodeGenerator` package described below, or by creating a C# project which can act as the target for code generated from assemblies written in other languages. +Both packages (`Microsoft.Orleans.CodeGenerator.MSBuild` and `Microsoft.Orleans.OrleansCodeGenerator.Build`) only support C# projects. Support other languages either by using the `Microsoft.Orleans.OrleansCodeGenerator` package (described below) or by creating a C# project acting as the target for code generated from assemblies written in other languages. -Additional diagnostics can be emitted at build time by specifying a value for `OrleansCodeGenLogLevel` in the target project's *.csproj* file. For example, `Trace`. +Emit additional diagnostics at build time by specifying a value for `OrleansCodeGenLogLevel` in the target project's *.csproj* file. For example: `Trace`. :::zone-end -### What happens during initialization? +### Initialization-time code generation :::zone target="docs" pivot="orleans-7-0" -In Orleans 7+, nothing happens during initialization. Code generation is performed at build time. +In Orleans 7+, nothing happens during initialization. Code generation occurs only at build time. :::zone-end @@ -90,8 +91,7 @@ builder.ConfigureApplicationParts( .WithCodeGeneration()); ``` -In the foregoing example, `builder` may be an instance of either or . -An optional instance can be passed to `WithCodeGeneration` to enable logging during code generation, for example: +In the preceding example, `builder` can be an instance of either or . Pass an optional instance to `WithCodeGeneration` to enable logging during code generation, for example: ```csharp ILoggerFactory codeGenLoggerFactory = new LoggerFactory(); @@ -110,7 +110,7 @@ codeGenLoggerFactory.AddProvider(new ConsoleLoggerProvider()); :::zone target="docs" pivot="orleans-7-0" -When applying to a type, you can also apply the to uniquely identify the member. Likewise, you may also apply an alias with the . For more information on influencing code generation, see [Use Orleans serialization](../host/configuration-guide/serialization.md#use-orleans-serialization). +When applying to a type, the can also be applied to uniquely identify the member. Likewise, an alias can be applied using the . For more information on influencing code generation, see [Use Orleans serialization](../host/configuration-guide/serialization.md#use-orleans-serialization). :::zone-end @@ -118,40 +118,40 @@ When applying to a type, you can also :::zone target="docs" pivot="orleans-3-x" -During code generation, you can influence generating code for a specific type. Code is automatically generated for grain interfaces, grain classes, grain state, and types passed as arguments in grain methods. If a type does not fit these criteria, the following methods can be used to further guide code generation. +During code generation, the generation of code for a specific type can be influenced. Orleans automatically generates code for grain interfaces, grain classes, grain state, and types passed as arguments in grain methods. If a type doesn't fit these criteria, use the following methods to guide code generation further. -Adding to a type instructs the code generator to generate a serializer for that type. +Adding to a type instructs the code generator to generate a serializer for it. -Adding [`[assembly: GenerateSerializer(Type)]`](xref:Orleans.CodeGeneration.GenerateSerializerAttribute) to a project instructs the code generator to treat that type as serializable and will cause an error if a serializer could not be generated for that type, for example, because the type is not accessible. This error will halt a build if code generation is enabled. This attribute also allows generating code for specific types from another assembly. +Adding [`[assembly: GenerateSerializer(Type)]`](xref:Orleans.CodeGeneration.GenerateSerializerAttribute) to a project instructs the code generator to treat that type as serializable. It causes an error if a serializer cannot be generated for that type (e.g., because the type isn't accessible). This error halts the build if code generation is enabled. This attribute also allows generating code for specific types from another assembly. -[`[assembly: KnownType(Type)]`](xref:Orleans.CodeGeneration.KnownTypeAttribute) also instructs the code generator to include a specific type (which may be from a referenced assembly), but does not cause an exception if the type is inaccessible. +[`[assembly: KnownType(Type)]`](xref:Orleans.CodeGeneration.KnownTypeAttribute) also instructs the code generator to include a specific type (which might be from a referenced assembly), but it doesn't cause an exception if the type is inaccessible. ### Generate serializers for all subtypes -Adding to an interface or class instructs the code generator to generate serialization code for all types which inherit/implement that type. +Adding to an interface or class instructs the code generator to generate serialization code for all types inheriting from or implementing that type. ### Generate code for all types in another assembly -There are cases where generated code cannot be included in a particular assembly at build time. For example, this can include shared libraries that do not reference Orleans, assemblies written in languages other than C#, and assemblies in which the developer does not have the source code. In these cases, generated code for those assemblies can be placed into a separate assembly which is referenced during initialization. +Sometimes, generated code cannot be included in a particular assembly at build time. Examples include shared libraries not referencing Orleans, assemblies written in languages other than C#, and assemblies for which the source code isn't available. In these cases, place the generated code for those assemblies into a separate assembly referenced during initialization. To enable this for an assembly: 1. Create a C# project. -1. Install the `Microsoft.Orleans.CodeGenerator.MSBuild` or the `Microsoft.Orleans.OrleansCodeGenerator.Build` package. +1. Install the `Microsoft.Orleans.CodeGenerator.MSBuild` or `Microsoft.Orleans.OrleansCodeGenerator.Build` package. 1. Add a reference to the target assembly. 1. Add `[assembly: KnownAssembly("OtherAssembly")]` at the top level of a C# file. -The instructs the code generator to inspect the specified assembly and generate code for the types within it. The attribute can be used multiple times within a project. +The instructs the code generator to inspect the specified assembly and generate code for the types within it. This attribute can be used multiple times within a project. -The generated assembly must then be added to the client/silo during initialization: +Then, add the generated assembly to the client/silo during initialization: ```csharp builder.ConfigureApplicationParts( parts => parts.AddApplicationPart("CodeGenAssembly")); ``` -In the foregoing example, `builder` may be an instance of either or . +In the preceding example, `builder` can be an instance of either or . -`KnownAssemblyAttribute` has an optional property, , which can be set to `true` to instruct the code generator to act as though all types within that assembly are marked as serializable. +`KnownAssemblyAttribute` has an optional property, . Set this to `true` to instruct the code generator to act as though all types within that assembly are marked as serializable. :::zone-end diff --git a/docs/orleans/grains/event-sourcing/event-sourcing-configuration.md b/docs/orleans/grains/event-sourcing/event-sourcing-configuration.md index ef5115964450d..05cea341fc997 100644 --- a/docs/orleans/grains/event-sourcing/event-sourcing-configuration.md +++ b/docs/orleans/grains/event-sourcing/event-sourcing-configuration.md @@ -1,30 +1,31 @@ --- title: Event sourcing configuration description: Learn about event sourcing configuration in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Event sourcing configuration -In this article, you'll learn about various event sourcing configuration options for .NET Orleans. +In this article, you learn about various event sourcing configuration options for .NET Orleans. ## Configure project references -### Grain Interfaces +### Grain interfaces -As before, interfaces depend only on the `Microsoft.Orleans.Core` package, because the grain interface is independent of the implementation. +As before, interfaces depend only on the `Microsoft.Orleans.Core` package because the grain interface is independent of the implementation. ### Grain implementations -JournaledGrains need to derive from or , which is defined in the `Microsoft.Orleans.EventSourcing` package. +Journaled grains need to derive from or , which is defined in the `Microsoft.Orleans.EventSourcing` package. ### Log-consistency providers -We currently include three log-consistency providers (for state storage, log storage, and custom storage). All three are contained in the `Microsoft.Orleans.EventSourcing` package as well. Therefore, all Journaled Grains already have access to those. For a description of what these providers do and how they differ, see [Included Log-Consistency Providers](log-consistency-providers.md). +We currently include three log-consistency providers (for state storage, log storage, and custom storage). All three are contained in the `Microsoft.Orleans.EventSourcing` package as well. Therefore, all journaled grains already have access to them. For a description of what these providers do and how they differ, see [Included log-consistency providers](log-consistency-providers.md). ## Cluster configuration -Log-consistency providers are configured just like any other Orleans providers. For example, to include all three providers (of course, you probably won't need all three), add this to the `` element of the configuration file: +Configure log-consistency providers just like any other Orleans providers. For example, to include all three providers (though you probably won't need all three), add this to the `` element of the configuration file: ```xml @@ -37,7 +38,7 @@ Log-consistency providers are configured just like any other Orleans providers. ``` -The same can be achieved programmatically. Moving forward to 2.0.0 stable, ClientConfiguration and ClusterConfiguration no longer exist! It has now been replaced by a and a `SiloBuilder` (notice there is no cluster builder). +You can achieve the same programmatically. Starting with Orleans 2.0.0 stable, `ClientConfiguration` and `ClusterConfiguration` no longer exist. They have been replaced by and `SiloBuilder` (note there's no cluster builder). ```csharp builder.AddLogStorageBasedLogConsistencyProvider("LogStorage") @@ -57,11 +58,11 @@ public class EventSourcedBankAccountGrain : } ``` -So here `"OrleansLocalStorage"` is being used for storing the grain state, where was `"LogStorage"` is the in-memory storage provider for EventSourcing events. +So here, `"OrleansLocalStorage"` is used for storing the grain state, whereas `"LogStorage"` is the in-memory storage provider for EventSourcing events. ### `LogConsistencyProvider` attributes -To specify the log-consistency provider, add a `[LogConsistencyProvider(ProviderName=...)]` attribute to the grain class, and give the name of the provider as configured by the cluster configuration, for example: +To specify the log-consistency provider, add a `[LogConsistencyProvider(ProviderName=...)]` attribute to the grain class and provide the name of the provider as configured in the cluster configuration, for example: ```csharp [LogConsistencyProvider(ProviderName = "CustomStorage")] @@ -74,7 +75,7 @@ public class ChatGrain : ### `StorageProvider` attributes -Some log-consistency providers (including `LogStorage` and `StateStorage`) use a standard StorageProvider to communicate with storage. This provider is specified using a separate `StorageProvider` attribute, as follows: +Some log-consistency providers (including `LogStorage` and `StateStorage`) use a standard `StorageProvider` to communicate with storage. Specify this provider using a separate `StorageProvider` attribute, as follows: ```csharp [LogConsistencyProvider(ProviderName = "LogStorage")] @@ -88,7 +89,7 @@ public class ChatGrain : ## Default providers -It is possible to omit the `LogConsistencyProvider` and/or the `StorageProvider` attributes, if a default is specified in the configuration. This is done by using the special name `Default` for the respective provider. For example: +You can omit the `LogConsistencyProvider` and/or `StorageProvider` attributes if a default is specified in the configuration. Do this by using the special name `Default` for the respective provider. For example: ```xml diff --git a/docs/orleans/grains/event-sourcing/immediate-vs-delayed-confirmation.md b/docs/orleans/grains/event-sourcing/immediate-vs-delayed-confirmation.md index be91e111b8df0..c91a6d2e0d9b8 100644 --- a/docs/orleans/grains/event-sourcing/immediate-vs-delayed-confirmation.md +++ b/docs/orleans/grains/event-sourcing/immediate-vs-delayed-confirmation.md @@ -1,59 +1,60 @@ --- title: Immediate and delayed confirmation description: Learn the differences between immediate and delayed confirmation in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Immediate and delayed confirmations -In this article, you'll learn the differences between immediate and delayed confirmations. +In this article, you learn the differences between immediate and delayed confirmations. ## Immediate confirmation -For many applications, we want to ensure that events are confirmed immediately, so that the persisted version does not lag behind the current version in memory, and we do not risk losing the latest state if the grain should fail. We can guarantee this by following these rules: +For many applications, you want to ensure events are confirmed immediately. This prevents the persisted version from lagging behind the current version in memory and avoids the risk of losing the latest state if the grain fails. You can guarantee this by following these rules: 1. Confirm all calls using before the grain method returns. -1. Make sure tasks returned by complete before the grain method returns. -1. Avoid or attributes, so only one grain call can be processed at a time. +1. Ensure tasks returned by complete before the grain method returns. +1. Avoid or attributes, so only one grain call processes at a time. -If we follow these rules, it means that after an event is raised, no other grain code can execute until the event has been written to storage. Therefore, it is impossible to observe inconsistencies between the version in memory and the version in storage. While this is often exactly what we want, it also has some potential disadvantages. +If you follow these rules, it means that after an event is raised, no other grain code can execute until the event has been written to storage. Therefore, it's impossible to observe inconsistencies between the in-memory version and the stored version. While this is often exactly what you want, it also has some potential disadvantages. ### Potential disadvantages -* If the connection to a remote cluster or storage is temporarily interrupted, then the grain becomes unavailable: effectively, the grain cannot execute any code while it is stuck waiting to confirm the events, which can take an indefinite amount of time (the log-consistency protocol keeps retrying until storage connectivity is restored). +- If the connection to a remote cluster or storage is temporarily interrupted, the grain becomes unavailable. Effectively, the grain cannot execute any code while stuck waiting to confirm events, which can take an indefinite amount of time (the log-consistency protocol keeps retrying until storage connectivity is restored). -* When handling a lot of updates to a single grain instance, confirming them one at a time can become very inefficient, for example, causing poor throughput. +- When handling many updates to a single grain instance, confirming them one at a time can become very inefficient, potentially causing poor throughput. ## Delayed confirmation To improve availability and throughput in the situations mentioned above, grains can choose to do one or both of the following: -* Allow grain methods to raise events without waiting for confirmation. -* Allow reentrancy, so the grain can keep processing new calls even if previous calls get stuck waiting for confirmation. +- Allow grain methods to raise events without waiting for confirmation. +- Allow reentrancy, so the grain can keep processing new calls even if previous calls get stuck waiting for confirmation. -This means grain code can execute while some events are still in the process of being confirmed. The API has some specific provisions to give developers precise control over how to deal with unconfirmed events that are currently _in flight_. +This means grain code can execute while some events are still being confirmed. The API has specific provisions giving you precise control over how to handle unconfirmed events currently _in flight_. -The following property can be examined to find out what events are currently unconfirmed: +You can examine the following property to find out which events are currently unconfirmed: ```csharp IEnumerable UnconfirmedEvents { get; } ``` -Also, since the state returned by the property does not include the effect of unconfirmed events, there is an alternative property +Also, since the state returned by the property doesn't include the effect of unconfirmed events, there's an alternative property: ```csharp StateType TentativeState { get; } ``` -Which returns a "tentative" state, obtained from "State" by applying all the unconfirmed events. The tentative state is essentially a "best guess" at what will likely become the next confirmed state after all unconfirmed events are confirmed. However, there is no guarantee that it actually will, because the grain may fail, or because the events may race against other events and lose, causing them to be canceled (if they are conditional) or appear at a later position in the sequence than anticipated (if they are unconditional). +This property returns a "tentative" state, obtained from `State` by applying all unconfirmed events. The tentative state is essentially a "best guess" at what will likely become the next confirmed state after all unconfirmed events are confirmed. However, there's no guarantee it actually will become the confirmed state. This is because the grain might fail, or the events might race against other events and lose, causing them to be canceled (if conditional) or appear later in the sequence than anticipated (if unconditional). ## Concurrency guarantees -Note that Orleans turn-based scheduling (cooperative concurrency) guarantees always apply, even when using reentrancy or delayed confirmation. This means that even though several methods may be in progress, only one can be actively executing—all others are stuck at an await, so there are never any true races caused by parallel threads. +Note that Orleans turn-based scheduling (cooperative concurrency) guarantees always apply, even when using reentrancy or delayed confirmation. This means that even though several methods might be in progress, only one can be actively executing—all others are stuck at an `await`. Therefore, there are never any true races caused by parallel threads. In particular, note that: -* The properties , , , and can change during the execution of a method. -* But such changes can only happen while stuck at an await. +- The properties , , , and can change during the execution of a method. +- However, such changes can only happen while stuck at an `await`. -These guarantees assume that the user code stays within the [recommended practice](../external-tasks-and-grains.md) concerning tasks and async/await (in particular, does not use thread pool tasks, or only uses them for code that does not call grain functionality and that are properly awaited). +These guarantees assume your code stays within the [recommended practices](../external-tasks-and-grains.md) concerning tasks and async/await (in particular, doesn't use thread pool tasks, or only uses them for code that doesn't call grain functionality and that are properly awaited). diff --git a/docs/orleans/grains/event-sourcing/index.md b/docs/orleans/grains/event-sourcing/index.md index 18fa1167aa99f..0b24569fcd6b8 100644 --- a/docs/orleans/grains/event-sourcing/index.md +++ b/docs/orleans/grains/event-sourcing/index.md @@ -1,27 +1,28 @@ --- title: Event sourcing overview description: Learn an overview of event sourcing in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: overview --- # Event sourcing overview -Event sourcing provides a flexible way to manage and persist the grain state. An event-sourced grain has many potential advantages over a standard grain. For one, it can be used with many different storage provider configurations and supports geo-replication across multiple clusters. Moreover, it cleanly separates the grain class from definitions of the grain state (represented by a grain state object) and grain updates (represented by event objects). +Event sourcing provides a flexible way to manage and persist grain state. An event-sourced grain has many potential advantages over a standard grain. For one, you can use it with many different storage provider configurations, and it supports geo-replication across multiple clusters. Moreover, it cleanly separates the grain class from definitions of the grain state (represented by a grain state object) and grain updates (represented by event objects). The documentation is structured as follows: -* [JournaledGrain Basics](journaledgrain-basics.md) explains how to define event-sourced grains by deriving from , how to access the current state, and how to raise events that update the state. +- [JournaledGrain basics](journaledgrain-basics.md) explains how to define event-sourced grains by deriving from , how to access the current state, and how to raise events that update the state. -* [Replicated Instances](replicated-instances.md) explains how the event-sourcing mechanism handles replicated grain instances and ensures consistency. It discusses the possibility of racing events and conflicts, and how to address them. +- [Replicated instances](replicated-instances.md) explains how the event-sourcing mechanism handles replicated grain instances and ensures consistency. It discusses the possibility of racing events and conflicts, and how to address them. -* [Immediate/Delayed Confirmation](immediate-vs-delayed-confirmation.md) explains how delayed confirmation of events, and reentrancy, can improve availability and throughput. +- [Immediate/Delayed confirmation](immediate-vs-delayed-confirmation.md) explains how delayed confirmation of events and reentrancy can improve availability and throughput. -* [Notifications](notifications.md) explains how to subscribe to notifications, allowing grains to react to new events. +- [Notifications](notifications.md) explains how to subscribe to notifications, allowing grains to react to new events. -* [Event Sourcing Configuration](event-sourcing-configuration.md) explains how to configure projects, clusters, and log-consistency providers. +- [Event sourcing configuration](event-sourcing-configuration.md) explains how to configure projects, clusters, and log-consistency providers. -* [Built-In Log-Consistency Providers](log-consistency-providers.md) explains how the three currently included log-consistency providers work. +- [Built-in log-consistency providers](log-consistency-providers.md) explains how the three currently included log-consistency providers work. -* [JournaledGrain Diagnostics](journaledgrain-diagnostics.md) explains how to monitor for connection errors, and get simple statistics. +- [JournaledGrain diagnostics](journaledgrain-diagnostics.md) explains how to monitor for connection errors and get simple statistics. -The behavior documented above is reasonably stable, as far as the JournaledGrain API is concerned. However, we expect to extend or change the list of log consistency providers soon, to more easily allow developers to plug in standard event storage systems. +The behavior documented above is reasonably stable regarding the `JournaledGrain` API. However, we expect to extend or change the list of log consistency providers soon to more easily allow you to plug in standard event storage systems. diff --git a/docs/orleans/grains/event-sourcing/journaledgrain-basics.md b/docs/orleans/grains/event-sourcing/journaledgrain-basics.md index 757f881172e06..a7b6bf77daeff 100644 --- a/docs/orleans/grains/event-sourcing/journaledgrain-basics.md +++ b/docs/orleans/grains/event-sourcing/journaledgrain-basics.md @@ -1,36 +1,37 @@ --- title: The JournaledGrain API description: Learn the concepts of the JournaledGrain API in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- -# JournaledGrain Basics +# JournaledGrain basics Journaled grains derive from , with the following type parameters: -* The `TGrainState` represents the state of the grain. It must be a class with a public default constructor. -* `TEventBase` is a common supertype for all the events that can be raised for this grain, and can be any class or interface. +- `TGrainState` represents the state of the grain. It must be a class with a public default constructor. +- `TEventBase` is a common supertype for all events that can be raised for this grain and can be any class or interface. -All state and event objects should be serializable (because the log-consistency providers may need to persist them, and/or send them in notification messages). +All state and event objects should be serializable because log-consistency providers might need to persist them and/or send them in notification messages. -For grains whose events are POCOs (plain old C# objects), can be used as a shorthand for . +For grains whose events are POCOs (plain old C# objects), you can use as a shorthand for . ## Reading the grain state -To read the current grain state, and determine its version number, the JournaledGrain has properties +To read the current grain state and determine its version number, `JournaledGrain` has these properties: ```csharp GrainState State { get; } int Version { get; } ``` -The version number is always equal to the total number of confirmed events, and the state is the result of applying all the confirmed events to the initial state. The initial state, which has version 0 (because no events have been applied to it), is determined by the default constructor of the GrainState class. +The version number always equals the total number of confirmed events, and the state is the result of applying all confirmed events to the initial state. The default constructor of the `GrainState` class determines the initial state, which has version 0 (because no events have been applied to it). -_Important:_ The application should never directly modify the object returned by `State`. It is meant for reading only. Rather, when the application wants to modify the state, it must do so indirectly by raising events. +_Important:_ Your application should never directly modify the object returned by `State`. It's meant for reading only. When your application needs to modify the state, it must do so indirectly by raising events. ## Raise events -Raising events is accomplished by calling the function. For example, a grain representing a chat can raise a `PostedEvent` to indicate that a user-submitted a post: +Raise events by calling the function. For example, a grain representing a chat can raise a `PostedEvent` to indicate that a user submitted a post: ```csharp RaiseEvent(new PostedEvent() @@ -42,7 +43,7 @@ RaiseEvent(new PostedEvent() }); ``` -Note that `RaiseEvent` kicks off a write to storage access, but does not wait for the write to complete. For many applications, it is important to wait until we have confirmation that the event has been persisted. In that case, we always follow up by waiting for : +Note that `RaiseEvent` initiates a write to storage but doesn't wait for the write to complete. For many applications, it's important to wait for confirmation that the event has been persisted. In that case, always follow up by waiting for : ```csharp RaiseEvent(new DepositTransaction() @@ -53,13 +54,13 @@ RaiseEvent(new DepositTransaction() await ConfirmEvents(); ``` -Note that even if you don't explicitly call `ConfirmEvents`, the events will eventually be confirmed - it happens automatically in the background. +Note that even if you don't explicitly call `ConfirmEvents`, the events eventually get confirmed automatically in the background. ## State transition methods -The runtime updates the grain state _automatically_ whenever events are raised. There is no need for the application to explicitly update the state after raising an event. However, the application still has to provide the code that specifies _how_ to update the state in response to an event. This can be done in two ways. +The runtime updates the grain state _automatically_ whenever events are raised. Your application doesn't need to explicitly update the state after raising an event. However, your application still needs to provide the code specifying _how_ to update the state in response to an event. You can do this in two ways: -**(a)** The class can implement one or more `Apply` methods on the `StateType`. Typically, one would create multiple overloads, and the closest match is chosen for the runtime type of the event: +**(a)** The `GrainState` class can implement one or more `Apply` methods on the `StateType`. Typically, you create multiple overloads, and the runtime chooses the closest match for the runtime type of the event: ```csharp class GrainState @@ -86,15 +87,15 @@ protected override void TransitionState( } ``` -The transition methods are assumed to have no side effects other than modifying the state object, and should be deterministic (otherwise, the effects are unpredictable). If the transition code throws an exception, that exception is caught and included in a warning in the Orleans log, issued by the log-consistency provider. +Assume transition methods have no side effects other than modifying the state object and should be deterministic (otherwise, the effects are unpredictable). If the transition code throws an exception, Orleans catches it and includes it in a warning in the Orleans log, issued by the log-consistency provider. -When, exactly, the runtime calls the transition methods depends on the chosen log consistency provider and its configuration. Applications shouldn't rely on a particular timing, except when specifically guaranteed by the log consistency provider. +When exactly the runtime calls the transition methods depends on the chosen log-consistency provider and its configuration. Applications shouldn't rely on specific timing unless the log-consistency provider explicitly guarantees it. -Some providers, such as the log-consistency provider, replay the event sequence every time the grain is loaded. Therefore, as long as the event objects can still be properly deserialized from storage, it is possible to radically modify the `GrainState` class and the transition methods. But for other providers, such as the log-consistency provider, only the `GrainState` object is persisted, so developers must ensure that it can be deserialized correctly when read from storage. +Some providers, like the log-consistency provider, replay the event sequence every time the grain loads. Therefore, as long as the event objects can still be properly deserialized from storage, you can radically modify the `GrainState` class and the transition methods. However, for other providers, such as the log-consistency provider, only the `GrainState` object is persisted. In this case, you must ensure it can be deserialized correctly when read from storage. ## Raise multiple events -It is possible to make multiple calls to `RaiseEvent` before calling `ConfirmEvents`: +You can make multiple calls to `RaiseEvent` before calling `ConfirmEvents`: ```csharp RaiseEvent(e1); @@ -102,17 +103,17 @@ RaiseEvent(e2); await ConfirmEvents(); ``` -However, this is likely to cause two successive storage accesses, and it incurs a risk that the grain fails after writing only the first event. Thus, it is usually better to raise multiple events at once, using +However, this likely causes two successive storage accesses and incurs a risk that the grain fails after writing only the first event. Thus, it's usually better to raise multiple events at once using: ```csharp RaiseEvents(IEnumerable events) ``` -This guarantees that the given sequence of events is written to storage atomically. Note that since the version number always matches the length of the event sequence, raising multiple events increases the version number by more than one at a time. +This guarantees the given sequence of events is written to storage atomically. Note that since the version number always matches the length of the event sequence, raising multiple events increases the version number by more than one at a time. ## Retrieve the event sequence -The following method from the base `JournaledGrain` class allows the application to retrieve a specified segment of the sequence of all confirmed events: +The following method from the base `JournaledGrain` class allows your application to retrieve a specified segment of the sequence of all confirmed events: ```csharp Task> RetrieveConfirmedEvents( @@ -120,14 +121,14 @@ Task> RetrieveConfirmedEvents( int toVersion); ``` -However, it is not supported by all log consistency providers. If not supported, or if the specified segment of the sequence is no longer available, a is thrown. +However, not all log-consistency providers support this method. If it's not supported, or if the specified segment of the sequence is no longer available, a is thrown. -To retrieve all events up to the latest confirmed version, one would call +To retrieve all events up to the latest confirmed version, call: ```csharp await RetrieveConfirmedEvents(0, Version); ``` -Only confirmed events can be retrieved: an exception is thrown if `toVersion` is larger than the current value of the property `Version`. +You can only retrieve confirmed events: an exception is thrown if `toVersion` is larger than the current value of the `Version` property. -Since confirmed events never change, there are no races to worry about, even in the presence of multiple instances or delayed confirmation. However, in such situations, the value of the property `Version` may be larger by the time the `await` resumes than at the time `RetrieveConfirmedEvents` is called, so it may be advisable to save its value in a variable. See also the section on Concurrency Guarantees. +Since confirmed events never change, there are no races to worry about, even with multiple instances or delayed confirmation. However, in such situations, the value of the `Version` property might be larger by the time the `await` resumes than when `RetrieveConfirmedEvents` was called. Therefore, it might be advisable to save its value in a variable. See also the section on [Concurrency Guarantees](immediate-vs-delayed-confirmation.md#concurrency-guarantees). diff --git a/docs/orleans/grains/event-sourcing/journaledgrain-diagnostics.md b/docs/orleans/grains/event-sourcing/journaledgrain-diagnostics.md index 4637940af71b4..7e1e8f016ccd6 100644 --- a/docs/orleans/grains/event-sourcing/journaledgrain-diagnostics.md +++ b/docs/orleans/grains/event-sourcing/journaledgrain-diagnostics.md @@ -1,16 +1,17 @@ --- title: JournaledGrain diagnostics description: Learn how to use JournaledGrain diagnostics in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # JournaledGrain diagnostics ## Monitor connection errors -By design, log consistency providers are resilient under connection errors (including both connections to storage, and connections between clusters). But just tolerating errors is not enough, as applications usually need to monitor any such issues, and bring them to the attention of an operator if they are serious. +By design, log-consistency providers are resilient under connection errors (including connections to storage and connections between clusters). However, merely tolerating errors isn't enough. Applications usually need to monitor such issues and bring them to an operator's attention if they are serious. -JournaledGrain subclasses can override the following methods to receive notifications when there are connection errors observed, and when those errors are resolved: +`JournaledGrain` subclasses can override the following methods to receive notifications when connection errors are observed and when those errors resolve: ```csharp protected override void OnConnectionIssue( @@ -26,20 +27,20 @@ protected override void OnConnectionIssueResolved( } ``` - is an abstract class, with several common fields describing the issue, including how many times it has been observed since the last time connection was successful. The actual type of connection issue is defined by subclasses. Connection issues are categorized into types, such as or , and sometimes have extra keys (such as ) that further narrow the category. + is an abstract class with several common fields describing the issue, including how many times it has been observed since the last successful connection. Subclasses define the actual type of connection issue. Connection issues are categorized into types, such as or , and sometimes have extra keys (like ) that further narrow the category. -If the same category of issue happens several times (for example, we keep getting a `NotificationFailed` that targets the same `RemoteCluster`), it is reported each time by . Once this category of issue is resolved (for example, we are finally successful with sending a notification to this `RemoteCluster`), then is called once, with the same `issue` object that was last reported by `OnConnectionIssue`. Connection issues, and their resolution, for independent categories, are reported independently. +If the same category of issue happens multiple times (for example, you keep getting a `NotificationFailed` targeting the same `RemoteCluster`), Orleans reports it each time via . Once this category of issue resolves (for example, you finally succeed in sending a notification to that `RemoteCluster`), Orleans calls once, with the same `issue` object last reported by `OnConnectionIssue`. Connection issues and their resolutions for independent categories are reported independently. ## Simple statistics -We currently offer simple support for basic statistics (in the future, we will probably replace this with a more standard telemetry mechanism). Statistics collection can be enabled or disabled for a JournaledGrain by calling: +We currently offer simple support for basic statistics (in the future, we'll likely replace this with a more standard telemetry mechanism). You can enable or disable statistics collection for a `JournaledGrain` by calling: ```csharp void EnableStatsCollection() void DisableStatsCollection() ``` -The statistics can be retrieved by calling: +Retrieve the statistics by calling: ```csharp LogConsistencyStatistics GetStats() diff --git a/docs/orleans/grains/event-sourcing/log-consistency-providers.md b/docs/orleans/grains/event-sourcing/log-consistency-providers.md index ba9e4c5da2e8d..60540a4e3fe6a 100644 --- a/docs/orleans/grains/event-sourcing/log-consistency-providers.md +++ b/docs/orleans/grains/event-sourcing/log-consistency-providers.md @@ -1,38 +1,39 @@ --- title: Log-consistency providers description: Learn about log-consistency providers in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 03/29/2025 +ms.topic: conceptual --- # Log-consistency providers -The `Microsoft.Orleans.EventSourcing` package includes several log-consistency providers that cover basic scenarios suitable to get started, and allow some extensibility. +The `Microsoft.Orleans.EventSourcing` package includes several log-consistency providers covering basic scenarios suitable for getting started and allowing some extensibility. ### State storage -The stores *grain state snapshots*, using a standard storage provider that can be configured independently. +The stores *grain state snapshots* using a standard storage provider that you can configure independently. -The data that is kept in storage is an object that contains both the grain state (as specified by the first type parameter to `JournaledGrain`) and some meta-data (the version number, and a special tag that is used to avoid duplication of events when storage accesses fail). +The data kept in storage is an object containing both the grain state (specified by the first type parameter to `JournaledGrain`) and some metadata (the version number and a special tag used to avoid event duplication when storage accesses fail). -Since the entire grain state is read/written every time we access storage, this provider is not suitable for objects whose grain state is very large. +Since the entire grain state is read/written every time storage is accessed, this provider isn't suitable for objects with very large grain states. -This provider does not support , it cannot retrieve the events from storage because the events are not persisted. +This provider doesn't support . It cannot retrieve events from storage because the events aren't persisted. ### Log storage -The stores *the complete event sequence as a single object*, using a standard storage provider that can be configured independently. +The stores *the complete event sequence as a single object* using a standard storage provider that you can configure independently. -The data that is kept in storage is an object that contains a `List object`, and some meta-data (a special tag that is used to avoid duplication of events when storage accesses fail). +The data kept in storage is an object containing a `List` object and some metadata (a special tag used to avoid event duplication when storage accesses fail). -This provider does support . All events are always available and kept in memory. +This provider supports . All events are always available and kept in memory. -Since the whole event sequence is read/written every time we access storage, this provider is *not suitable for use in production*, unless the event sequences are guaranteed to remain pretty short. The main purpose of this provider is to illustrate the semantics of the event sourcing, and for samples/testing environments. +Since the entire event sequence is read/written every time storage is accessed, this provider is *not suitable for production use* unless the event sequences are guaranteed to remain fairly short. The main purpose of this provider is to illustrate the semantics of event sourcing and for use in samples/testing environments. ### Custom storage -This allows the developer to plug in their storage interface, which is then called by the consistency protocol at appropriate times. This provider does not make specific assumptions about whether what is stored are state snapshots or events - the programmer assumes control over that choice (and may store either or both). +This allows you to plug in your storage interface, which the consistency protocol then calls at appropriate times. This provider doesn't make specific assumptions about whether the stored data consists of state snapshots or events – you assume control over that choice (and can store either or both). -To use this provider, a grain must derive from , as before, but additionally must also implement the following interface: +To use this provider, a grain must derive from , as before, but must also implement the following interface: ```csharp public interface ICustomStorageInterface @@ -45,12 +46,12 @@ public interface ICustomStorageInterface } ``` -The consistency provider expects these to behave a certain way. Programmers should be aware that: +The consistency provider expects these methods to behave in a certain way. Be aware that: -* The first method, , is expected to return both the version and the state read. If there is nothing stored yet, it should return zero for the version, and a state that matches corresponds to the default constructor for `StateType`. +- The first method, , is expected to return both the version and the state read. If nothing is stored yet, it should return zero for the version and a state corresponding to the default constructor for `StateType`. -* must return false if the expected version does not match the actual version (this is analogous to an e-tag check). +- must return `false` if the expected version doesn't match the actual version (this is analogous to an e-tag check). -* If `ApplyUpdatesToStorage` fails with an exception, the consistency provider retries. This means some events could be duplicated if such an exception is thrown, but the event was persisted. The developer is responsible to make sure this is safe: for example, either avoid this case by not throwing an exception, or ensure duplicated events are harmless for the application logic, or add some extra mechanism to filter duplicates. +- If `ApplyUpdatesToStorage` fails with an exception, the consistency provider retries. This means some events could be duplicated if such an exception is thrown but the event was persisted. You are responsible for ensuring this is safe: for example,, either avoid this case by not throwing an exception, ensure duplicated events are harmless for the application logic, or add an extra mechanism to filter duplicates. -This provider does not support `RetrieveConfirmedEvents`. Of course, since the developer controls the storage interface anyway, they don't need to call this in the first place, but can implement their event retrieval. +This provider doesn't support `RetrieveConfirmedEvents`. Of course, since you control the storage interface anyway, you don't need to call this method in the first place but can implement your event retrieval logic. diff --git a/docs/orleans/grains/event-sourcing/notifications.md b/docs/orleans/grains/event-sourcing/notifications.md index ca8285150faf5..47c30e0dfb70a 100644 --- a/docs/orleans/grains/event-sourcing/notifications.md +++ b/docs/orleans/grains/event-sourcing/notifications.md @@ -1,17 +1,17 @@ --- title: Notifications description: Learn the concepts of notifications in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Notifications -It is often convenient to have the ability to react to state changes. -All callbacks are subject to Orleans' turn-based guarantees; see also the section on Concurrency Guarantees. +It's often convenient to react to state changes. All callbacks are subject to Orleans' turn-based guarantees; see also the section on [Concurrency Guarantees](immediate-vs-delayed-confirmation.md#concurrency-guarantees). ## Track confirmed state -To be notified of any changes to the confirmed state, subclasses can override this method: +To be notified of any changes to the confirmed state, `JournaledGrain` subclasses can override this method: ```csharp protected override void OnStateChanged() @@ -20,13 +20,13 @@ protected override void OnStateChanged() } ``` -`OnStateChanged` is called whenever the confirmed state is updated, i.e. the version number increases. This can happen when +`OnStateChanged` is called whenever the confirmed state updates, i.e., the version number increases. This can happen when: 1. A newer version of the state was loaded from storage. -1. An event that was raised by this instance has been successfully written to storage. +1. An event raised by this instance has been successfully written to storage. 1. A notification message was received from some other instance. -Note that since all grains initially have version zero, until the initial load from storage completes, this means that is called whenever the initial load completes with a version larger than zero. +Note that since all grains initially have version zero, `OnStateChanged` is called whenever the initial load from storage completes with a version larger than zero. ## Track tentative state @@ -39,4 +39,4 @@ protected override void OnTentativeStateChanged() } ``` - is called whenever the tentative state changes, i.e. if the combined sequence (ConfirmedEvents + UnconfirmedEvents) changes. In particular, a callback to `OnTentativeStateChanged()` always happens during . +`OnTentativeStateChanged` is called whenever the tentative state changes, i.e., if the combined sequence (`ConfirmedEvents` + `UnconfirmedEvents`) changes. In particular, a callback to `OnTentativeStateChanged()` always happens during . diff --git a/docs/orleans/grains/event-sourcing/replicated-instances.md b/docs/orleans/grains/event-sourcing/replicated-instances.md index 5446c51a44709..ffcb1416e0230 100644 --- a/docs/orleans/grains/event-sourcing/replicated-instances.md +++ b/docs/orleans/grains/event-sourcing/replicated-instances.md @@ -1,47 +1,48 @@ --- title: Replicated grains -description: Learn the concepts of the replicated grains in .NET Orleans. -ms.date: 07/03/2024 +description: Learn the concepts of replicated grains in .NET Orleans. +ms.date: 05/23/2025 +ms.topic: conceptual --- # Replicated grains -Sometimes, there can be multiple instances of the same grain active, such as when operating a multi-cluster, and using the . The JournaledGrain is designed to support replicated instances with minimal friction. It relies on *log-consistency providers* to run the necessary protocols to ensure all instances agree on the same sequence of events. In particular, it takes care of the following aspects: +Sometimes, multiple instances of the same grain can be active, such as when operating a multi-cluster and using the . `JournaledGrain` is designed to support replicated instances with minimal friction. It relies on *log-consistency providers* to run the necessary protocols ensuring all instances agree on the same sequence of events. In particular, it handles the following aspects: -* **Consistent Versions**: All versions of the grain state (except for tentative versions) are based on the same global sequence of events. In particular, if two instances see the same version number, then they see the same state. +- **Consistent versions**: All versions of the grain state (except tentative versions) are based on the same global sequence of events. In particular, if two instances see the same version number, they see the same state. -* **Racing Events**: Multiple instances can simultaneously raise an event. The consistency provider resolves this race and ensures everyone agrees on the same sequence. +- **Racing events**: Multiple instances can simultaneously raise an event. The consistency provider resolves this race and ensures all instances agree on the same sequence. -* **Notifications/Reactivity**: After an event is raised at one-grain instance, the consistency provider not only updates storage but also notifies all the other grain instances. +- **Notifications/Reactivity**: After an event is raised at one grain instance, the consistency provider not only updates storage but also notifies all other grain instances. -For a general discussion of the consistency, model see our [TechReport](https://www.microsoft.com/research/publication/geo-distribution-actor-based-services/) and the [GSP paper](https://www.microsoft.com/research/publication/global-sequence-protocol-a-robust-abstraction-for-replicated-shared-state-extended-version/) (Global Sequence Protocol). +For a general discussion of the consistency model, see our [TechReport](https://www.microsoft.com/research/publication/geo-distribution-actor-based-services/) and the [GSP paper](https://www.microsoft.com/research/publication/global-sequence-protocol-a-robust-abstraction-for-replicated-shared-state-extended-version/) (Global Sequence Protocol). ## Conditional events -Racing events can be problematic if they have a conflict, i.e. should not both commit for some reason. For example, when withdrawing money from a bank account, two instances may independently determine that there are sufficient funds for a withdrawal, and issue a withdrawal event. But the combination of both events could overdraw. To avoid this, the `JournaledGrain` API supports a method. +Racing events can be problematic if they conflict, i.e., both shouldn't commit for some reason. For example, when withdrawing money from a bank account, two instances might independently determine sufficient funds exist for a withdrawal and issue a withdrawal event. However, the combination of both events could overdraw the account. To avoid this, the `JournaledGrain` API supports the method. ```csharp bool success = await RaiseConditionalEvent( new WithdrawalEvent() { /* ... */ }); ``` -Conditional events double-check if the local version matches the version in storage. If not, it means the event sequence has grown in the meantime, which means this event has lost a race against some other event. In that case, the conditional event is *not* appended to the log, and returns false. +Conditional events double-check if the local version matches the version in storage. If not, it means the event sequence has grown in the meantime, indicating this event lost a race against another event. In that case, the conditional event is *not* appended to the log, and returns `false`. -This is the analog of using e-tags with conditional storage updates, and likewise provides a simple mechanism to avoid committing conflicting events. +This is analogous to using e-tags with conditional storage updates and provides a simple mechanism to avoid committing conflicting events. -It is possible and sensible to use both conditional and unconditional events for the same grain, such as a `DepositEvent` and a `WithdrawalEvent`. Deposits need not be conditional: even if a `DepositEvent` loses a race, it does not have to be cancelled, but can still be appended to the global event sequence. +It's possible and sensible to use both conditional and unconditional events for the same grain, such as `DepositEvent` and `WithdrawalEvent`. Deposits don't need to be conditional: even if a `DepositEvent` loses a race, it doesn't have to be canceled but can still be appended to the global event sequence. -Awaiting the task returned by `RaiseConditionalEvent` is sufficient to confirm the event, i.e. it is not necessary to also call `ConfirmEvents`. +Awaiting the task returned by `RaiseConditionalEvent` is sufficient to confirm the event; you don't need to call `ConfirmEvents` as well. ## Explicit synchronization -Sometimes, it is desirable to ensure that a grain is fully caught up with the latest version. This can be enforced by calling: +Sometimes, you might want to ensure a grain is fully caught up with the latest version. You can enforce this by calling: ```csharp await RefreshNow(); ``` -This does two things: +This call does two things: -1. It confirms all unconfirmed events. -1. It loads the latest version from storage. +1. Confirms all unconfirmed events. +1. Loads the latest version from storage. diff --git a/docs/orleans/grains/external-tasks-and-grains.md b/docs/orleans/grains/external-tasks-and-grains.md index c5575096e7304..24a4a58df4697 100644 --- a/docs/orleans/grains/external-tasks-and-grains.md +++ b/docs/orleans/grains/external-tasks-and-grains.md @@ -1,35 +1,36 @@ --- title: External tasks and grains description: Learn about external tasks and grains in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 03/31/2025 +ms.topic: conceptual --- # External tasks and grains -By design, any sub-Tasks spawned from grain code (for example, by using `await` or `ContinueWith` or `Task.Factory.StartNew`) will be dispatched on the same per-activation as the parent task and therefore inherit the same *single-threaded execution model* as the rest of grain code. This is the main point behind the single-threaded execution of grain turn-based concurrency. +By design, any sub-tasks spawned from grain code (e.g., using `await`, `ContinueWith`, or `Task.Factory.StartNew`) dispatch on the same per-activation as the parent task. Therefore, they inherit the same *single-threaded execution model* as the rest of the grain code. This is the main point behind the single-threaded execution of grain turn-based concurrency. -In some cases grain code might need to "break out" of the Orleans task scheduling model and "do something special", such as explicitly pointing a `Task` to a different task scheduler or the .NET . An example of such a case is when grain code has to execute a synchronous remote blocking call (such as remote IO). Executing that blocking call in the grain context will block the grain and thus should never be made. Instead, the grain code can execute this piece of blocking code on the thread pool thread and join (`await`) the completion of that execution and proceed in the grain context. We expect that escaping from the Orleans scheduler will be a very advanced and seldom-required usage scenario beyond the "normal" usage patterns. +In some cases, grain code might need to "break out" of the Orleans task scheduling model and "do something special," such as explicitly pointing a `Task` to a different task scheduler or the .NET . An example is when grain code needs to execute a synchronous remote blocking call (like remote I/O). Executing that blocking call in the grain context blocks the grain and thus should never be done. Instead, the grain code can execute this piece of blocking code on a thread pool thread, join (`await`) the completion of that execution, and then proceed in the grain context. Escaping the Orleans scheduler is expected to be a very advanced and seldom-required usage scenario beyond typical usage patterns. ## Task-based APIs -1. [await](../../csharp/language-reference/operators/await.md), (see below), , , , all respect the current task scheduler. That means that using them in the default way, without passing a different , will cause them to execute in the grain context. +1. `await`, (see below), , , , and all respect the current task scheduler. This means using them in the default way, without passing a different , causes them to execute in the grain context. -1. Both and the `endMethod` delegate of do *not* respect the current task scheduler. They both use the `TaskScheduler.Default` scheduler, which is the .NET thread pool task scheduler. Therefore, the code inside `Task.Run` and the `endMethod` in `Task.Factory.FromAsync` will *always* run on the .NET thread pool outside of the single-threaded execution model for Orleans grains. However, any code after the `await Task.Run` or `await Task.Factory.FromAsync` will run back under the scheduler at the point the task was created, which is the grain's scheduler. +1. Both and the `endMethod` delegate of do *not* respect the current task scheduler. They both use the `TaskScheduler.Default` scheduler, the .NET thread pool task scheduler. Therefore, code inside `Task.Run` and the `endMethod` in `Task.Factory.FromAsync` *always* runs on the .NET thread pool, outside the single-threaded execution model for Orleans grains. However, any code after `await Task.Run` or `await Task.Factory.FromAsync` runs back under the scheduler active at the point the task was created, which is the grain's scheduler. -1. with `false` is an explicit API to escape the current task scheduler. It will cause the code after an awaited Task to be executed on the scheduler, which is the .NET thread pool, and will thus break the single-threaded execution of the grain. +1. with `false` is an explicit API to escape the current task scheduler. It causes the code after an awaited `Task` to execute on the scheduler (the .NET thread pool), thus breaking the single-threaded execution of the grain. > [!CAUTION] - > You should in general **never use `ConfigureAwait(false)` directly in grain code.** + > Generally, **never use `ConfigureAwait(false)` directly in grain code.** -1. Methods with the signature `async void` should not be used with grains. They are intended for graphical user interface event handlers. `async void` method can immediately crash the current process if they allow an exception to escape, with no way of handling the exception. This is also true for `List.ForEach(async element => ...)` and any other method which accepts an , since the asynchronous delegate will be coerced into an `async void` delegate. +1. Methods with the signature `async void` should not be used with grains. They are intended for graphical user interface event handlers. An `async void` method can immediately crash the current process if it allows an exception to escape, with no way to handle the exception. This also applies to `List.ForEach(async element => ...)` and any other method accepting an , since the asynchronous delegate coerces into an `async void` delegate. ### `Task.Factory.StartNew` and `async` delegates -The usual recommendation for scheduling tasks in any C# program is to use `Task.Run` in favor of `Task.Factory.StartNew`. A quick google search on the use of `Task.Factory.StartNew` will suggest [that it is dangerous](https://blog.stephencleary.com/2013/08/startnew-is-dangerous.html) and [that one should always favor `Task.Run`](https://devblogs.microsoft.com/pfxteam/task-run-vs-task-factory-startnew/). But if we want to stay in the grain's *single-threaded execution model* for our grain then we need to use it, so how do we do it correctly then? The danger when using `Task.Factory.StartNew()` is that it does not natively support async delegates. This means that this is likely a bug: `var notIntendedTask = Task.Factory.StartNew(SomeDelegateAsync)`. `notIntendedTask` is *not* a task that completes when `SomeDelegateAsync` does. Instead, one should *always* unwrap the returned task: `var task = Task.Factory.StartNew(SomeDelegateAsync).Unwrap()`. +The usual recommendation for scheduling tasks in C# is using `Task.Run` instead of `Task.Factory.StartNew`. A quick web search for `Task.Factory.StartNew` suggests [it's dangerous](https://blog.stephencleary.com/2013/08/startnew-is-dangerous.html) and [favoring `Task.Run` is always recommended](https://devblogs.microsoft.com/pfxteam/task-run-vs-task-factory-startnew/). However, to stay within the grain's *single-threaded execution model*, `Task.Factory.StartNew` must be used. So, how to use it correctly? The danger with `Task.Factory.StartNew()` is its lack of native support for async delegates. This means code like `var notIntendedTask = Task.Factory.StartNew(SomeDelegateAsync)` is likely a bug. `notIntendedTask` is *not* a task completing when `SomeDelegateAsync` finishes. Instead, *always* unwrap the returned task: `var task = Task.Factory.StartNew(SomeDelegateAsync).Unwrap()`. -#### Example multiple tasks and the task scheduler +#### Example: Multiple tasks and the task scheduler -Below is sample code that demonstrates the usage of `TaskScheduler.Current`, `Task.Run`, and a special custom scheduler to escape from Orleans grain context and how to get back to it. +Below is sample code demonstrating using `TaskScheduler.Current`, `Task.Run`, and a special custom scheduler to escape the Orleans grain context and how to return to it. ```csharp public async Task MyGrainMethod() @@ -76,11 +77,11 @@ public async Task MyGrainMethod() } ``` -#### Example make a grain call from code that runs on a thread pool +#### Example: Make a grain call from code running on a thread pool thread -Another scenario is a piece of grain code that needs to "break out" of the grain's task scheduling model and run on a thread pool (or some other, non-grain context), but still needs to call another grain. Grain calls can be made from non-grain contexts without extra ceremony. +Another scenario involves grain code needing to "break out" of the grain's task scheduling model and run on a thread pool thread (or some other non-grain context) but still needing to call another grain. Grain calls can be made from non-grain contexts without extra ceremony. -The following is code that demonstrates how a grain call can be made from a piece of code that runs inside a grain but not in the grain context. +The following code demonstrates making a grain call from code running inside a grain but not in the grain context. ```csharp public async Task MyGrainMethod() @@ -112,25 +113,25 @@ public async Task MyGrainMethod() ## Work with libraries -Some external libraries that your code is using might be using `ConfigureAwait(false)` internally. It is a good and correct practice in .NET to use `ConfigureAwait(false)` [when implementing general-purpose libraries](https://devblogs.microsoft.com/dotnet/configureawait-faq/#when-should-i-use-configureawaitfalse). This is not a problem in Orleans. As long as the code in the grain that invokes the library method is awaiting the library call with a regular `await`, the grain code is correct. The result will be exactly as desired – the library code will run continuations on the default scheduler (the value returned by `TaskScheduler.Default`, which does not guarantee that the continuations will run on a thread as continuations are often inlined in the previous thread), while the grain code will run on the grain's scheduler. +Some external libraries used by the code might use `ConfigureAwait(false)` internally. Using `ConfigureAwait(false)` is a good and correct practice in .NET [when implementing general-purpose libraries](https://devblogs.microsoft.com/dotnet/configureawait-faq/#when-should-i-use-configureawaitfalse). This isn't a problem in Orleans. As long as the grain code invoking the library method awaits the library call with a regular `await`, the grain code is correct. The result is exactly as desired: the library code runs continuations on the default scheduler (the value returned by `TaskScheduler.Default`, which doesn't guarantee continuations run on a thread, as they are often inlined on the previous thread), while the grain code runs on the grain's scheduler. -Another frequently asked question is whether there is a need to execute library calls with `Task.Run`—that is, whether there is a need to explicitly offload the library code to `ThreadPool` (for grain code to do `Task.Run(() => myLibrary.FooAsync())`). The answer is no. There is no need to offload any code to `ThreadPool` except for the case of library code that is making a blocking synchronous calls. Usually, any well-written and correct .NET async library (methods that return `Task` and are named with an `Async` suffix) doesn't make blocking calls. Thus there is no need to offload anything to `ThreadPool` unless you suspect the async library is buggy or if you are deliberately using a synchronous blocking library. +Another frequently asked question is whether library calls need executing with `Task.Run`—that is, whether library code needs explicit offloading to the `ThreadPool` (e.g., `await Task.Run(() => myLibrary.FooAsync())`). The answer is no. Offloading code to the `ThreadPool` isn't necessary except when library code makes blocking synchronous calls. Usually, any well-written and correct .NET async library (methods returning `Task` and named with an `Async` suffix) doesn't make blocking calls. Thus, offloading anything to the `ThreadPool` isn't needed unless the async library is suspected to be buggy or a synchronous blocking library is deliberately used. ## Deadlocks -Since grains execute in a single-threaded fashion, it is possible to deadlock a grain by synchronously blocking in a way that would require multiple threads to unblock. This means that code that calls any of the following methods and properties can deadlock a grain if the provided tasks have not yet been completed by the time the method or property is invoked: +Since grains execute single-threaded, deadlocking a grain is possible by synchronously blocking in a way requiring multiple threads to unblock. This means code calling any of the following methods and properties can deadlock a grain if the provided tasks haven't completed by the time the method or property is invoked: -* `Task.Wait()` -* `Task.Result` -* `Task.WaitAny(...)` -* `Task.WaitAll(...)` -* `task.GetAwaiter().GetResult()` +- `Task.Wait()` +- `Task.Result` +- `Task.WaitAny(...)` +- `Task.WaitAll(...)` +- `task.GetAwaiter().GetResult()` -These methods should be avoided in any high-concurrency service because they can lead to poor performance and instability by starving the .NET `ThreadPool` by blocking threads that could be performing useful work and requiring the .NET `ThreadPool` to inject additional threads so that they can be completed. When executing grain code, these methods, as mentioned above, can cause the grain to deadlock, and therefore they should also be avoided in grain code. +Avoid these methods in any high-concurrency service because they can lead to poor performance and instability. They starve the .NET `ThreadPool` by blocking threads that could perform useful work and require the `ThreadPool` to inject additional threads for completion. When executing grain code, these methods can cause the grain to deadlock, so avoid them in grain code as well. -If there is some *sync-over-async* work that cannot be avoided, it is best to move that work to a separate scheduler. The simplest way to do this is to use `await Task.Run(() => task.Wait())` for example. Please note that it is strongly recommended to avoid *sync-over-async* work since, as mentioned above, it will cause your application's scalability and performance to suffer. +If some *sync-over-async* work is unavoidable, moving that work to a separate scheduler is best. The simplest way is using `await Task.Run(() => task.Wait())`, for example. Note that avoiding *sync-over-async* work is strongly recommended, as it harms application scalability and performance. -## Summary working with Tasks in Orleans +## Summary: Working with tasks in Orleans | What are you trying to do? | How to do it | |--|--| diff --git a/docs/orleans/grains/grain-extensions.md b/docs/orleans/grains/grain-extensions.md index 8ed2343472eb5..5a35ddd6e7230 100644 --- a/docs/orleans/grains/grain-extensions.md +++ b/docs/orleans/grains/grain-extensions.md @@ -1,22 +1,23 @@ --- title: Grain extensions description: Learn how to extend an Orleans Grain. -ms.date: 07/03/2024 +ms.date: 03/31/2025 +ms.topic: conceptual --- # Grain extensions -Grain extensions provide a way to add extra behavior to grains. By extending a grain with an interface that derives from , you can add new methods and functionality to the grain. +Grain extensions provide a way to add extra behavior to grains. By extending a grain with an interface deriving from , new methods and functionality can be added to the grain. -In this article, you see two examples of grain extensions. The first example shows how to add a `Deactivate` method to all grains that can be used to deactivate the grain. The second example shows how to add a `GetState` and `SetState` method to any grain, allowing you to manipulate the grain's internal state. +This article presents two examples of grain extensions. The first example shows how to add a `Deactivate` method to all grains, usable for deactivating the grain. The second example shows how to add `GetState` and `SetState` methods to any grain, allowing manipulation of the grain's internal state. ## Deactivate extension example -In this example, you will learn how to add a `Deactivate` method to all grains automatically. The method can be used to deactivate the grain and accepts a string as a message parameter. Orleans grains already support this functionality via the interface. Nevertheless, this example serves to show how you could add this or similar functionality yourself. +This example demonstrates adding a `Deactivate` method to all grains automatically. Use the method to deactivate the grain; it accepts a string as a message parameter. Orleans grains already support this functionality via the interface. Nevertheless, this example shows how this or similar functionality could be added. ### Deactivate extension interface -Start by defining an `IGrainDeactivateExtension` interface, which contains the `Deactivate` method. The interface must derive from `IGrainExtension`. +Start by defining an `IGrainDeactivateExtension` interface containing the `Deactivate` method. The interface must derive from `IGrainExtension`. ```csharp public interface IGrainDeactivateExtension : IGrainExtension @@ -27,9 +28,9 @@ public interface IGrainDeactivateExtension : IGrainExtension ### Deactivate extension implementation -Next, implement the `GrainDeactivateExtension` class, which provides the implementation for the `Deactivate` method. +Next, implement the `GrainDeactivateExtension` class, providing the implementation for the `Deactivate` method. -To access the target grain, you retrieve the `IGrainContext` from the constructor. It's injected when creating the extension with dependency injection. +To access the target grain, retrieve the `IGrainContext` from the constructor. It's injected via dependency injection when creating the extension. ```csharp public sealed class GrainDeactivateExtension : IGrainDeactivateExtension @@ -52,13 +53,13 @@ public sealed class GrainDeactivateExtension : IGrainDeactivateExtension ### Deactivate extension registration and usage -Now that you've defined the interface and implementation, you register the extension when configuring the silo with the method. +Now that the interface and implementation are defined, register the extension when configuring the silo using the method. ```csharp siloBuilder.AddGrainExtension(); ``` -To use the extension on any grain, retrieve a reference to the extension and call the `Deactivate` method. +To use the extension on any grain, retrieve a reference to the extension and call its `Deactivate` method. ```csharp var grain = client.GetGrain(someKey); @@ -69,11 +70,11 @@ await grainReferenceAsInterface.Deactivate("Because, I said so..."); ## State manipulation extension example -In this example, you learn how to add a `GetState` and `SetState` method to any grain through extensions, allowing you to manipulate the grain's internal state. +This example demonstrates adding `GetState` and `SetState` methods to any grain through extensions, allowing manipulation of the grain's internal state. ### State manipulation extension interface -First, define the `IGrainStateAccessor` interface, which contains the `GetState` and `SetState` methods. Again, this interface must derive from `IGrainExtension`. +First, define the `IGrainStateAccessor` interface containing the `GetState` and `SetState` methods. Again, this interface must derive from `IGrainExtension`. ```csharp public interface IGrainStateAccessor : IGrainExtension @@ -83,11 +84,11 @@ public interface IGrainStateAccessor : IGrainExtension } ``` -Once you have access to the target grain, you can use the extension to manipulate its state. In this example, you use an extension to access and modify a specific integer state value within the target grain. +Once access to the target grain is available, use the extension to manipulate its state. In this example, use an extension to access and modify a specific integer state value within the target grain. ### State manipulation extension implementation -The extension you use is `IGrainStateAccessor`, which provides methods to get and set a state value of type `T`. To create the extension, you implement the interface in a class that takes a `getter` and a `setter` as arguments in its constructor. +The extension used is `IGrainStateAccessor`, providing methods to get and set a state value of type `T`. To create the extension, implement the interface in a class taking a `getter` and a `setter` as arguments in its constructor. ```csharp public sealed class GrainStateAccessor : IGrainStateAccessor @@ -114,11 +115,11 @@ public sealed class GrainStateAccessor : IGrainStateAccessor } ``` -In the preceding implementation, the `GrainStateAccessor` class takes `getter` and `setter` arguments in its constructor. These delegates are used to read and modify the target grain's state. The `GetState()` method returns a wraps the current value of the `T` state, while the `SetState(T state)` method sets the new value of the `T` state. +In the preceding implementation, the `GrainStateAccessor` class takes `getter` and `setter` arguments in its constructor. These delegates read and modify the target grain's state. The `GetState()` method returns a wrapping the current value of the `T` state, while the `SetState(T state)` method sets the new value of the `T` state. ### State manipulation extension registration and usage -To use the extension to access and modify the target grain's state, you need to register the extension and set its components in the method of the target grain. +To use the extension to access and modify the target grain's state, register the extension and set its components in the target grain's method. ```csharp public override Task OnActivateAsync() @@ -135,9 +136,9 @@ public override Task OnActivateAsync() } ``` -In the preceding example, you create a new instance of `GrainStateAccessor` that takes a `getter` and a `setter` for an integer state value. The `getter` reads the `Value` property of the target grain, while the `setter` sets the new value of the `Value` property. you then set this instance as a component of the target grain's context using the method. +In the preceding example, create a new instance of `GrainStateAccessor` taking a `getter` and `setter` for an integer state value. The `getter` reads the `Value` property of the target grain, while the `setter` sets the new value of the `Value` property. Then, set this instance as a component of the target grain's context using the method. -Once the extension is registered, you can use it to get and set the target grain's state by accessing it through a reference to the extension. +Once the extension is registered, use it to get and set the target grain's state by accessing it through a reference to the extension. ```csharp // Get a reference to the IGrainStateAccessor extension @@ -150,7 +151,7 @@ var value = await accessor.GetState(); await accessor.SetState(10); ``` -In the preceding example, you get a reference to the `IGrainStateAccessor` extension for a specific grain instance using the method. you can then use this reference to call the `GetState()` and `SetState(T state)` methods to read and modify the state value of the target grain. +In the preceding example, get a reference to the `IGrainStateAccessor` extension for a specific grain instance using the method. Then, use this reference to call the `GetState()` and `SetState(T state)` methods to read and modify the state value of the target grain. ## See also diff --git a/docs/orleans/grains/grain-identity.md b/docs/orleans/grains/grain-identity.md index 92ad863268efc..f0a630aa39a4f 100644 --- a/docs/orleans/grains/grain-identity.md +++ b/docs/orleans/grains/grain-identity.md @@ -1,27 +1,28 @@ --- title: Grain identity description: Learn about grain identities in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 03/31/2025 +ms.topic: conceptual --- # Grain identity -Grains in Orleans each have a single, unique, user-defined identifier which consists of two parts: +Grains in Orleans each have a single, unique, user-defined identifier consisting of two parts: -1. The grain _type_ name, which uniquely identifies the grain class. -2. The grain _key_, which uniquely identifies a logical instance of that grain class. +1. The grain _type_ name, uniquely identifying the grain class. +1. The grain _key_, uniquely identifying a logical instance of that grain class. -The grain type and key are both represented as human-readable strings in Orleans and, by convention, the grain identity is written with the grain type and key separated by a `/` character. For example, `shoppingcart/bob65` represents the grain type named `shoppingcart` with a key `bob65`. +Orleans represents both grain type and key as human-readable strings. By convention, write the grain identity with the grain type and key separated by a `/` character. For example, `shoppingcart/bob65` represents the grain type named `shoppingcart` with the key `bob65`. -It's not common to construct grain identities directly. Instead, it's more common to create [grain references](./grain-references.md) using . +Constructing grain identities directly is uncommon. Instead, creating [grain references](./grain-references.md) using is more common. The following sections discuss grain type names and grain keys in more detail. ## Grain type names -Orleans creates a grain type name for you based on your grain implementation class by removing the suffix "Grain" from the class name, if it's present, and converting the resulting string into its lower-case representation. For example, a class named `ShoppingCartGrain` will be given the grain type name `shoppingcart`. It's recommended that grain type names and keys consist only of printable characters such as alpha-numeric (`a`-`z`, `A`-`Z`, and `0`-`9`) characters and symbols such as `-`, `_`, `@`, `=`. Other characters may or may not be supported and will often need special treatment when printed in logs or appearing as identifiers in other systems such as databases. +Orleans creates a grain type name based on the grain implementation class. It removes the suffix "Grain" from the class name, if present, and converts the resulting string to its lowercase representation. For example, a class named `ShoppingCartGrain` receives the grain type name `shoppingcart`. It's recommended that grain type names and keys consist only of printable characters, such as alphanumeric characters (`a`-`z`, `A`-`Z`, and `0`-`9`) and symbols like `-`, `_`, `@`, and `=`. Other characters might not be supported and often require special treatment when printed in logs or appearing as identifiers in other systems, such as databases. -Alternatively, you can use the attribute to customize the grain type name for the grain class to which it is attached, as in the following example: +Alternatively, use the attribute to customize the grain type name for the grain class it's attached to, as shown in the following example: ```csharp [GrainType("cart")] @@ -31,9 +32,9 @@ public class ShoppingCartGrain : IShoppingCartGrain } ``` -In the preceding example, the grain class, `ShoppingCartGrain` has a grain type name of `cart`. Each grain can only have one grain type name. +In the preceding example, the grain class `ShoppingCartGrain` has the grain type name `cart`. Each grain can only have one grain type name. -For generic grains, the generic arity must be included in the grain type name. For example, consider the following `DictionaryGrain` class: +For generic grains, include the generic arity in the grain type name. For example, consider the following `DictionaryGrain` class: ```csharp [GrainType("dict`2")] @@ -43,29 +44,27 @@ public class DictionaryGrain : IDictionaryGrain } ``` -The grain class has two generic parameters, so a backtick `` ` `` followed by the generic arity, 2, is added to the end of the grain type name, `dict` to create the grain type name ``dict`2``, as specified in the attribute on the grain class, ``[GrainType("dict`2")]``. +The grain class has two generic parameters, so a backtick `` ` `` followed by the generic arity, 2, is added to the end of the grain type name `dict` to create the grain type name ``dict`2``. This is specified in the attribute on the grain class: `[GrainType("dict`2")]`. ## Grain keys -For convenience, Orleans exposes methods which allow construction of grain keys from a or a , in addition to a . -The primary key is scoped to the grain type. -Therefore, the complete identity of a grain is formed from the grain's type and its key. +For convenience, Orleans exposes methods allowing construction of grain keys from a or , in addition to a . The primary key is scoped to the grain type. Therefore, the complete identity of a grain forms from its type and key. -The caller of the grain decides which scheme should be used. The options are: +The caller of the grain decides which scheme to use. The options are: -* -* -* -* and -* and +- +- +- +- and +- and Because the underlying data is the same, the schemes can be used interchangeably: they are all encoded as strings. -Situations that require a singleton grain instance can use a well-known, fixed value such as `"default"`. This is merely a convention, but by adhering to this convention it becomes clear at the caller site that a singleton grain is in use. +Situations requiring a singleton grain instance can use a well-known, fixed value such as `"default"`. This is merely a convention, but adhering to it clarifies at the caller site that a singleton grain is in use. ### Using globally unique identifiers (GUIDs) as keys - make useful keys when randomness and global uniqueness are desired, such as when creating a new job in a job processing system. You don't need to coordinate the allocation of keys, which could introduce a single point of failure in the system, or a system-side lock on a resource that could present a bottleneck. There is a very low chance of GUIDs colliding, so they are a common choice when architecting a system which needs to allocate random identifiers. + make useful keys when randomness and global uniqueness are desired, such as when creating a new job in a job processing system. Coordinating key allocation isn't needed, which could introduce a single point of failure or a system-side lock on a resource, potentially creating a bottleneck. The chance of GUID collisions is very low, making them a common choice when architecting systems needing random identifier allocation. Referencing a grain by GUID in client code: @@ -85,7 +84,7 @@ public override Task OnActivateAsync() ### Using integers as keys -A long integer is also available, which would make sense if the grain is persisted to a relational database, where numerical indexes are preferred over GUIDs. +A long integer is also available. This makes sense if the grain persists to a relational database, where numerical indexes are often preferred over GUIDs. Referencing a grain by a long integer in client code: @@ -105,7 +104,7 @@ public override Task OnActivateAsync() ### Using strings as keys -A string is also available. +A string key is also available. Referencing a grain by String in client code: @@ -125,9 +124,9 @@ public override Task OnActivateAsync() ### Using compound keys -If you have a system that doesn't fit well with either GUIDs or longs, you can opt for a compound primary key, which allows you to use a combination of a GUID or long and a string to reference a grain. +If the system doesn't fit well with either GUIDs or longs, opt for a compound primary key. This allows using a combination of a GUID or long and a string to reference a grain. -You can inherit your interface from or interface like this: +Inherit the interface from or like this: ```csharp public interface IExampleGrain : Orleans.IGrainWithIntegerCompoundKey @@ -142,7 +141,7 @@ In client code, this adds a second argument to the (0, "a string!", null); ``` -To access the compound key in the grain, we can call an overload on the method (the ): +To access the compound key in the grain, call an overload of the method (such as ): ```csharp public class ExampleGrain : Orleans.Grain, IExampleGrain @@ -159,4 +158,4 @@ public class ExampleGrain : Orleans.Grain, IExampleGrain ## Why grains use logical identifiers -In object-oriented environments, such as .NET, the identity of an object is hard to distinguish from a reference to it. When an object is created using the `new` keyword, the reference you get back represents all aspects of its identity except those that map the object to some external entity that it represents. Orleans is designed for distributed systems. In distributed systems, object references cannot represent instance identity since object references are limited to a single process' address space. Orleans uses logical identifiers to avoid this limitation. Grain use logical identifiers so that grain references remain valid across process lifetimes and are portable from one process to another, allowing them to be stored and later retrieved or to be sent across a network to another process in the application, all while still referring to the same entity: the grain which the reference was created for. +In object-oriented environments like .NET, an object's identity is hard to distinguish from a reference to it. When an object is created using the `new` keyword, the returned reference represents all aspects of its identity except those mapping the object to some external entity it represents. Orleans is designed for distributed systems. In distributed systems, object references cannot represent instance identity because they are limited to a single process's address space. Orleans uses logical identifiers to avoid this limitation. Grains use logical identifiers so grain references remain valid across process lifetimes and are portable from one process to another. This allows them to be stored and later retrieved, or sent across a network to another process in the application, all while still referring to the same entity: the grain for which the reference was created. diff --git a/docs/orleans/grains/grain-lifecycle.md b/docs/orleans/grains/grain-lifecycle.md index 071b966b0f7f6..61ebf048e2280 100644 --- a/docs/orleans/grains/grain-lifecycle.md +++ b/docs/orleans/grains/grain-lifecycle.md @@ -1,17 +1,18 @@ --- title: Grain lifecycle overview description: Learn about grain lifecycles in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 03/31/2025 +ms.topic: conceptual zone_pivot_groups: orleans-version --- # Grain lifecycle overview -Orleans grains use an observable lifecycle (See [Orleans Lifecycle](../implementation/orleans-lifecycle.md)) for ordered activation and deactivation. This allows grain logic, system components, and application logic to be started and stopped in an ordered manner during grain activation and collection. +Orleans grains use an observable lifecycle (see [Orleans Lifecycle](../implementation/orleans-lifecycle.md)) for ordered activation and deactivation. This allows grain logic, system components, and application logic to start and stop in an ordered manner during grain activation and collection. ## Stages -The pre-defined grain lifecycle stages are as follows. +The predefined grain lifecycle stages are as follows: ```csharp public static class GrainLifecycleStage @@ -24,21 +25,21 @@ public static class GrainLifecycleStage ``` - `First`: First stage in a grain's lifecycle. -- `SetupState`: Setup grain state, before activation. For stateful grains, this is the stage where is loaded from storage, when is `true`. -- `Activate`: Stage where and are called. +- `SetupState`: Set up grain state before activation. For stateful grains, this is the stage where Orleans loads from storage if is `true`. +- `Activate`: Stage where Orleans calls and . - `Last`: Last stage in a grain's lifecycle. -While the grain lifecycle will be used during grain activation, since grains are not always deactivated during some error cases (such as silo crashes), applications should not rely on the grain lifecycle always being executed during grain deactivations. +While Orleans uses the grain lifecycle during grain activation, grains aren't always deactivated during some error cases (such as silo crashes). Therefore, applications shouldn't rely on the grain lifecycle always executing during grain deactivations. ## Grain lifecycle participation -Application logic can participate with a grain's lifecycle in two ways: +Application logic can participate in a grain's lifecycle in two ways: :::zone target="docs" pivot="orleans-7-0" -- The grain can participate in its lifecycle. +- The grain can participate in its own lifecycle. - Components can access the lifecycle via the grain activation context (see ). :::zone-end @@ -47,8 +48,8 @@ Application logic can participate with a grain's lifecycle in two ways: :::zone target="docs" pivot="orleans-3-x" -- The grain can participate in its lifecycle. -- Components can access the lifecycle via the grain activation context (see ). +- The grain can participate in its own lifecycle. +- Components can access the lifecycle via the grain activation context (see ). :::zone-end @@ -67,13 +68,13 @@ public override void Participate(IGrainLifecycle lifecycle) } ``` -In the above example, overrides the method to tell the lifecycle to call its `OnSetupState` method during the stage of the lifecycle. +In the preceding example, overrides the method to tell the lifecycle to call its `OnSetupState` method during the stage of the lifecycle. :::zone target="docs" pivot="orleans-7-0" -Components created during a grain's construction can take part in the lifecycle as well, without the addition of any special grain logic. Since the grain's context (), including the grain's lifecycle (), is created before the grain is created, any component injected into the grain by the container can participate in the grain's lifecycle. +Components created during a grain's construction can also participate in the lifecycle without adding any special grain logic. Since Orleans creates the grain's context (), including its lifecycle (), before creating the grain, any component injected into the grain by the container can participate in the grain's lifecycle. :::zone-end @@ -81,13 +82,13 @@ Components created during a grain's construction can take part in the lifecycle :::zone target="docs" pivot="orleans-3-x" -Components created during a grain's construction can take part in the lifecycle as well, without the addition of any special grain logic. Since the grain's activation context (), including the grain's lifecycle (), is created before the grain is created, any component injected into the grain by the container can participate in the grain's lifecycle. +Components created during a grain's construction can also participate in the lifecycle without adding any special grain logic. Since Orleans creates the grain's activation context (), including its lifecycle (), before creating the grain, any component injected into the grain by the container can participate in the grain's lifecycle. :::zone-end -### Example participation, creation, and activation +### Example: Component participation -The following component participates in the grain's lifecycle when created using its factory function `Create(...)`. This logic could exist in the component's constructor, but that risks the component being added to the lifecycle before it's fully constructed, which may not be safe. +The following component participates in the grain's lifecycle when created using its factory function `Create(...)`. This logic could exist in the component's constructor, but that risks adding the component to the lifecycle before it's fully constructed, which might not be safe. :::zone target="docs" pivot="orleans-7-0" @@ -145,7 +146,7 @@ public class MyComponent : ILifecycleParticipant :::zone-end -By registering the example component in the service container using its `Create(...)` factory function, any grain constructed with the component as a dependency will have the component taking part in its lifecycle without any special logic in the grain. +By registering the example component in the service container using its `Create(...)` factory function, any grain constructed with the component as a dependency has the component participate in its lifecycle without requiring any special logic in the grain itself. #### Register component in container diff --git a/docs/orleans/grains/grain-persistence/azure-cosmos-db.md b/docs/orleans/grains/grain-persistence/azure-cosmos-db.md index f1e894e7dd2ae..fa64f64bfeb97 100644 --- a/docs/orleans/grains/grain-persistence/azure-cosmos-db.md +++ b/docs/orleans/grains/grain-persistence/azure-cosmos-db.md @@ -1,9 +1,9 @@ --- title: Azure Cosmos DB grain persistence -description: Use the Azure Cosmos DB grain to persist Azure Cosmos DB for NoSQL data in a .NET Orleans application. -ms.topic: conceptual +description: Use the Azure Cosmos DB grain persistence provider to persist Azure Cosmos DB for NoSQL data in a .NET Orleans application. +ms.topic: how-to ms.devlang: csharp -ms.date: 07/03/2024 +ms.date: 05/23/2025 --- # Azure Cosmos DB for NoSQL grain persistence @@ -19,7 +19,7 @@ Install the [Microsoft.Orleans.Persistence.Cosmos](https://www.nuget.org/package ## Configure clustering provider -To configure the clustering provider, use the `HostingExtensions.UseCosmosClustering` extension method. You can customize the name and throughput of the database or container, enable resource creation, or configure the client's credentials in this method. +To configure the clustering provider, use the `HostingExtensions.UseCosmosClustering` extension method. In this method, you can customize the name and throughput of the database or container, enable resource creation, or configure the client's credentials. ```csharp siloBuilder.UseCosmosClustering( diff --git a/docs/orleans/grains/grain-persistence/azure-storage.md b/docs/orleans/grains/grain-persistence/azure-storage.md index 6093f4597ab45..d91ceaba4e25a 100644 --- a/docs/orleans/grains/grain-persistence/azure-storage.md +++ b/docs/orleans/grains/grain-persistence/azure-storage.md @@ -1,18 +1,19 @@ --- title: Azure Storage grain persistence description: Learn about Azure Storage grain persistence in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: how-to --- # Azure Storage grain persistence The Azure Storage grain persistence provider supports both [Azure Blob Storage](/azure/storage/blobs/storage-blobs-introduction) and [Azure Table Storage](/azure/storage/common/storage-introduction?toc=/azure/storage/blobs/toc.json#table-storage). -## Install Azure Table Storage +## Configure Azure Table Storage -Install the [Microsoft.Orleans.Persistence.AzureStorage](https://www.nuget.org/packages/Microsoft.Orleans.Persistence.AzureStorage) package from NuGet. The Azure Table Storage provider stores state in a table row, splitting the state over multiple columns if the limits of a single column are exceeded. Each row can hold a maximum length of 1 megabyte, as [imposed by Azure Table Storage](/azure/storage/common/storage-scalability-targets#azure-table-storage-scale-targets). +Install the [Microsoft.Orleans.Persistence.AzureStorage](https://www.nuget.org/packages/Microsoft.Orleans.Persistence.AzureStorage) package from NuGet. The Azure Table Storage provider stores state in a table row, splitting the state across multiple columns if it exceeds the limits of a single column. Each row can hold a maximum of 1 megabyte, as [imposed by Azure Table Storage](/azure/storage/common/storage-scalability-targets#azure-table-storage-scale-targets). -Configure the Azure Table Storage grain persistence provider using the extension methods. +Configure the Azure Table Storage grain persistence provider using the extension method. ```csharp siloBuilder.AddAzureTableGrainStorage( @@ -24,11 +25,11 @@ siloBuilder.AddAzureTableGrainStorage( }); ``` -## Install Azure Blob Storage +## Configure Azure Blob Storage The Azure Blob Storage provider stores state in a blob. -Configure the Azure Blob Storage grain persistence provider using the extension methods. +Configure the Azure Blob Storage grain persistence provider using the extension method. ```csharp siloBuilder.AddAzureBlobGrainStorage( diff --git a/docs/orleans/grains/grain-persistence/dynamodb-storage.md b/docs/orleans/grains/grain-persistence/dynamodb-storage.md index 2172178fb3460..45901bd959fce 100644 --- a/docs/orleans/grains/grain-persistence/dynamodb-storage.md +++ b/docs/orleans/grains/grain-persistence/dynamodb-storage.md @@ -1,12 +1,13 @@ --- title: Amazon DynamoDB grain persistence -description: Learn about Azure DynamoDB grain persistence in .NET Orleans. -ms.date: 07/03/2024 +description: Learn about Amazon DynamoDB grain persistence in .NET Orleans. +ms.date: 05/23/2025 +ms.topic: how-to --- # Amazon DynamoDB grain persistence -In this article, you'll learn how to install and configure the Amazon DynamoDB grain persistence. +In this article, you learn how to install and configure Amazon DynamoDB grain persistence. ## Installation @@ -14,7 +15,7 @@ Install the [`Microsoft.Orleans.Persistence.DynamoDB`](https://www.nuget.org/pac ## Configuration -Configure the DynamoDB grain persistence provider using the extension methods. +Configure the DynamoDB grain persistence provider using the extension method. ```csharp siloBuilder.AddDynamoDBGrainStorage( @@ -28,13 +29,13 @@ siloBuilder.AddDynamoDBGrainStorage( ); ``` -If your authentication method requires a token or non-default profile name, you can define those properties using the following command: +If your authentication method requires a token or a non-default profile name, you can define those properties. First, view your credentials file using the following command: ```bash cat ~/.aws/credentials ``` -As an example, the following command will configure the DynamoDB grain persistence provider to use the `default` profile from the `~/.aws/credentials` file: +As an example, the following configuration shows how to configure the DynamoDB grain persistence provider to use the `default` profile from the `~/.aws/credentials` file: ```bash [YOUR_PROFILE_NAME] @@ -45,7 +46,7 @@ aws_session_expiration = *** aws_session_token = *** ``` -This allows for both types of authentication credentials: +This configuration allows for both types of authentication credentials: - access key & secret key - access key & secret key & token diff --git a/docs/orleans/grains/grain-persistence/index.md b/docs/orleans/grains/grain-persistence/index.md index 5ba0e17744946..6bd6b923e5721 100644 --- a/docs/orleans/grains/grain-persistence/index.md +++ b/docs/orleans/grains/grain-persistence/index.md @@ -1,37 +1,38 @@ --- title: Grain persistence -description: Learn about persistence in .NET Orleans. -ms.date: 09/10/2024 +description: Learn about grain persistence in .NET Orleans. +ms.date: 05/23/2025 +ms.topic: overview zone_pivot_groups: orleans-version --- # Grain persistence -Grains can have more than one named persistent data objects associated with them. These state objects are loaded from storage during grain activation so that they're available during requests. Grain persistence uses an extensible plugin model so that storage providers for any database can be used. This persistence model is designed for simplicity, and isn't intended to cover all data access patterns. Grains can also access databases directly, without using the grain persistence model. +Grains can have multiple named persistent data objects associated with them. These state objects load from storage during grain activation so they're available during requests. Grain persistence uses an extensible plugin model, allowing you to use storage providers for any database. This persistence model is designed for simplicity and isn't intended to cover all data access patterns. Grains can also access databases directly without using the grain persistence model. :::image type="content" source="media/grain-state-diagram.png" alt-text="Grain persistence diagram" lightbox="media/grain-state-diagram.png"::: -In the above diagram, UserGrain has a *Profile* state and a *Cart* state, each of which is stored in a separate storage system. +In the preceding diagram, UserGrain has a *Profile* state and a *Cart* state, each stored in a separate storage system. ## Goals -1. Multiple named persistent data objects per grain. -1. Multiple configured storage providers, each of which can have a different configuration and be backed by a different storage system. -1. Storage providers can be developed and published by the community. -1. Storage providers have complete control over how they store grain state data in the persistent backing store. Corollary: Orleans isn't providing a comprehensive ORM storage solution, but instead allows custom storage providers to support specific ORM requirements as and when required. +1. Support multiple named persistent data objects per grain. +1. Allow multiple configured storage providers, each potentially having a different configuration and backed by a different storage system. +1. Enable the community to develop and publish storage providers. +1. Give storage providers complete control over how they store grain state data in the persistent backing store. Corollary: Orleans doesn't provide a comprehensive ORM storage solution but allows custom storage providers to support specific ORM requirements as needed. ## Packages -Orleans grain storage providers can be found on [NuGet](https://www.nuget.org/packages?q=Orleans+Persistence). Officially maintained packages include: +You can find Orleans grain storage providers on [NuGet](https://www.nuget.org/packages?q=Orleans+Persistence). Officially maintained packages include: -- [Microsoft.Orleans.Persistence.AdoNet](https://www.nuget.org/packages/Microsoft.Orleans.Persistence.AdoNet) is for SQL databases and other storage systems supported by ADO.NET. For more information, see [ADO.NET Grain Persistence](relational-storage.md). -- [Microsoft.Orleans.Persistence.AzureStorage](https://www.nuget.org/packages/Microsoft.Orleans.Persistence.AzureStorage) is for Azure Storage, including Azure Blob Storage, and Azure Table Storage, via the Azure Table Storage API. For more information, see [Azure Storage Grain Persistence](azure-storage.md). -- [Microsoft.Orleans.Persistence.Cosmos](https://www.nuget.org/packages/Microsoft.Orleans.Persistence.Cosmos) is the Azure Cosmos DB provider. -- [Microsoft.Orleans.Persistence.DynamoDB](https://www.nuget.org/packages/Microsoft.Orleans.Persistence.DynamoDB) is for Amazon DynamoDB. For more information, see [Amazon DynamoDB Grain Persistence](dynamodb-storage.md). +- [Microsoft.Orleans.Persistence.AdoNet](https://www.nuget.org/packages/Microsoft.Orleans.Persistence.AdoNet): For SQL databases and other storage systems supported by ADO.NET. For more information, see [ADO.NET grain persistence](relational-storage.md). +- [Microsoft.Orleans.Persistence.AzureStorage](https://www.nuget.org/packages/Microsoft.Orleans.Persistence.AzureStorage): For Azure Storage, including Azure Blob Storage and Azure Table Storage (via the Azure Table Storage API). For more information, see [Azure Storage grain persistence](azure-storage.md). +- [Microsoft.Orleans.Persistence.Cosmos](https://www.nuget.org/packages/Microsoft.Orleans.Persistence.Cosmos): The Azure Cosmos DB provider. For more information, see [Azure Cosmos DB grain persistence](azure-cosmos-db.md). +- [Microsoft.Orleans.Persistence.DynamoDB](https://www.nuget.org/packages/Microsoft.Orleans.Persistence.DynamoDB): For Amazon DynamoDB. For more information, see [Amazon DynamoDB grain persistence](dynamodb-storage.md). ## API -Grains interact with their persistent state using where `TState` is the serializable state type: +Grains interact with their persistent state using , where `TState` is the serializable state type: :::zone target="docs" pivot="orleans-7-0" @@ -83,7 +84,7 @@ public interface IPersistentState where TState : new() :::zone-end -Instances of `IPersistentState` are injected into the grain as constructor parameters. These parameters can be annotated with a attribute to identify the name of the state being injected and the name of the storage provider that provides it. The following example demonstrates this by injecting two named states into the `UserGrain` constructor: +Orleans injects instances of `IPersistentState` into the grain as constructor parameters. You can annotate these parameters with a attribute to identify the name of the state being injected and the name of the storage provider supplying it. The following example demonstrates this by injecting two named states into the `UserGrain` constructor: ```csharp public class UserGrain : Grain, IUserGrain @@ -101,27 +102,27 @@ public class UserGrain : Grain, IUserGrain } ``` -Different grain types can use different configured storage providers, even if both are the same type; for example, two different Azure Table Storage provider instances, connected to different Azure Storage accounts. +Different grain types can use different configured storage providers, even if both are the same type (e.g., two different Azure Table Storage provider instances connected to different Azure Storage accounts). ### Read state -Grain state will automatically be read when the grain is activated, but grains are responsible for explicitly triggering the write for any changed grain state when necessary. +Grain state automatically reads when the grain activates, but grains are responsible for explicitly triggering the write for any changed grain state when necessary. -If a grain wishes to explicitly re-read the latest state for this grain from the backing store, the grain should call the method. This will reload the grain state from the persistent store via the storage provider, and the previous in-memory copy of the grain state will be overwritten and replaced when the `Task` from `ReadStateAsync()` completes. +If a grain wishes to explicitly re-read its latest state from the backing store, it should call the method. This reloads the grain state from the persistent store via the storage provider. The previous in-memory copy of the grain state is overwritten and replaced when the `Task` from `ReadStateAsync()` completes. -The value of the state is accessed using the `State` property. For example, the following method accesses the profile state declared in the code above: +Access the value of the state using the `State` property. For example, the following method accesses the profile state declared in the code above: ```csharp public Task GetNameAsync() => Task.FromResult(_profile.State.Name); ``` -There's no need to call `ReadStateAsync()` during normal operation; the state is loaded automatically during activation. However, `ReadStateAsync()` can be used to refresh state, which is modified externally. +There's no need to call `ReadStateAsync()` during normal operation; Orleans loads the state automatically during activation. However, you can use `ReadStateAsync()` to refresh state modified externally. -See the [Failure Modes](#FailureModes) section below for details of error-handling mechanisms. +See the [Failure modes](#failure-modes) section below for details on error-handling mechanisms. ### Write state -The state can be modified via the `State` property. The modified state isn't automatically persisted. Instead, the developer decides when to persist state by calling the method. For example, the following method updates a property on `State` and persists the updated state: +You can modify the state via the `State` property. The modified state isn't automatically persisted. Instead, you decide when to persist state by calling the method. For example, the following method updates a property on `State` and persists the updated state: ```csharp public async Task SetNameAsync(string name) @@ -131,17 +132,17 @@ public async Task SetNameAsync(string name) } ``` -Conceptually, the Orleans Runtime will take a deep copy of the grain state data object for its use during any write operations. Under the covers, the runtime *may* use optimization rules and heuristics to avoid performing some or all of the deep copy in some circumstances, if the expected logical isolation semantics are preserved. +Conceptually, the Orleans Runtime takes a deep copy of the grain state data object for its use during any write operations. Under the covers, the runtime *might* use optimization rules and heuristics to avoid performing some or all of the deep copy in certain circumstances, provided the expected logical isolation semantics are preserved. -See the [Failure Modes](#FailureModes) section below for details of error handling mechanisms. +See the [Failure modes](#failure-modes) section below for details on error handling mechanisms. ### Clear state -The method clears the grain's state in storage. Depending on the provider, this operation may optionally delete the grain state entirely. +The method clears the grain's state in storage. Depending on the provider, this operation might optionally delete the grain state entirely. ## Get started -Before a grain can use persistence, a storage provider must be configured on the silo. +Before a grain can use persistence, you must configure a storage provider on the silo. First, configure storage providers, one for profile state and one for cart state: @@ -212,16 +213,16 @@ var host = new HostBuilder() :::zone-end -Now that a storage provider has been configured with the name `"profileStore"`, we can access this provider from a grain. +Now that you've configured a storage provider named `"profileStore"`, you can access this provider from a grain. -The persistent state can be added to a grain in two primary ways: +You can add persistent state to a grain in two primary ways: 1. By injecting `IPersistentState` into the grain's constructor. -2. By inheriting from . +1. By inheriting from . -The recommended way to add storage to a grain is by injecting `IPersistentState` into the grain's constructor with an associated `[PersistentState("stateName", "providerName")]` attribute. For details on [`Grain`, see below](#using-graintstate-to-add-storage-to-a-grain). This is still supported but is considered a legacy approach. +The recommended way to add storage to a grain is by injecting `IPersistentState` into the grain's constructor with an associated `[PersistentState("stateName", "providerName")]` attribute. For details on `Grain`, see [Using `Grain` to add storage to a grain](#using-graintstate-to-add-storage-to-a-grain) below. Using `Grain` is still supported but considered a legacy approach. -Declare a class to hold our grain's state: +Declare a class to hold your grain's state: ```csharp [Serializable] @@ -252,7 +253,7 @@ public class UserGrain : Grain, IUserGrain > [!IMPORTANT] > The profile state will not be loaded at the time it is injected into the constructor, so accessing it is invalid at that time. The state will be loaded before is called. -Now that the grain has a persistent state, we can add methods to read and write the state: +Now that the grain has a persistent state, you can add methods to read and write the state: ```csharp public class UserGrain : Grain, IUserGrain @@ -276,34 +277,34 @@ public class UserGrain : Grain, IUserGrain } ``` -## Failure modes for persistence operations +## Failure modes for persistence operations ### Failure modes for read operations -Failures returned by the storage provider during the initial read of state data for that particular grain fails the activate operation for that grain; in such case, there won't be any call to that grain's `OnActivateAsync` life cycle callback method. The original request to the grain, which caused the activation will be faulted back to the caller, the same way as any other failure during grain activation. Failures encountered by the storage provider when reading state data for a particular grain result in an exception from `ReadStateAsync` `Task`. The grain can choose to handle or ignore the `Task` exception, just like any other `Task` in Orleans. +Failures returned by the storage provider during the initial read of state data for a particular grain fail the activation operation for that grain. In such cases, there won't be any call to that grain's `OnActivateAsync` lifecycle callback method. The original request to the grain that caused the activation faults back to the caller, just like any other failure during grain activation. Failures encountered by the storage provider when reading state data for a particular grain result in an exception from the `ReadStateAsync` `Task`. The grain can choose to handle or ignore the `Task` exception, just like any other `Task` in Orleans. -Any attempt to send a message to a grain that failed to load at silo startup time due to a missing/bad storage provider config returns the permanent error . +Any attempt to send a message to a grain that failed to load at silo startup due to a missing or bad storage provider configuration returns the permanent error . ### Failure modes for write operations -Failures encountered by the storage provider when writing state data for a particular grain results in an exception thrown by `WriteStateAsync()` `Task`. Usually, this means that the grain call exception will be thrown back to the client caller, provided the `WriteStateAsync()` `Task` is correctly chained into the final return `Task` for this grain method. However, it's possible in certain advanced scenarios to write grain code to specifically handle such write errors, just like they can handle any other faulted `Task`. +Failures encountered by the storage provider when writing state data for a particular grain result in an exception thrown by the `WriteStateAsync()` `Task`. Usually, this means the grain call exception is thrown back to the client caller, provided the `WriteStateAsync()` `Task` is correctly chained into the final return `Task` for this grain method. However, in certain advanced scenarios, you can write grain code to specifically handle such write errors, just like handling any other faulted `Task`. -Grains that execute error-handling / recovery code *must* catch exceptions / faulted `WriteStateAsync()` `Task`s and not rethrow them, to signify that they have successfully handled the write error. +Grains executing error-handling or recovery code *must* catch exceptions or faulted `WriteStateAsync()` `Task`s and not rethrow them, signifying they have successfully handled the write error. ## Recommendations ### Use JSON serialization or another version-tolerant serialization format -Code evolves and this often includes storage types, too. To accommodate these changes, an appropriate serializer should be configured. For most storage providers, a `UseJson` option or similar is available to use JSON as a serialization format. Ensure that when evolving data contracts already-stored data will still be loadable. +Code evolves, and this often includes storage types. To accommodate these changes, configure an appropriate serializer. For most storage providers, a `UseJson` option or similar is available to use JSON as the serialization format. Ensure that when evolving data contracts, already-stored data can still be loaded. -## Using Grain<TState> to add storage to a grain +## Using `Grain` to add storage to a grain > [!IMPORTANT] -> Using `Grain` to add storage to a grain is considered *legacy* functionality: grain storage should be added using `IPersistentState` as previously described. +> Using `Grain` to add storage to a grain is considered *legacy* functionality. Add grain storage using `IPersistentState` as previously described. -Grain classes that inherit from `Grain` (where `T` is an application-specific state data type that needs to be persisted) will have their state loaded automatically from specified storage. +Grain classes inheriting from `Grain` (where `T` is an application-specific state data type needing persistence) have their state loaded automatically from the specified storage. -Such grains are marked with a that specifies a named instance of a storage provider to use for reading / writing the state data for this grain. +Mark such grains with a specifying a named instance of a storage provider to use for reading/writing the state data for this grain. ```csharp [StorageProvider(ProviderName="store1")] @@ -313,7 +314,7 @@ public class MyGrain : Grain, /*...*/ } ``` -The `Grain` base class defined the following methods for subclasses to call: +The `Grain` base class defines the following methods for subclasses to call: ```csharp protected virtual Task ReadStateAsync() { /*...*/ } @@ -325,7 +326,7 @@ The behavior of these methods corresponds to their counterparts on `IPersistentS ## Create a storage provider -There are two parts to the state persistence APIs: the API exposed to the grain via `IPersistentState` or `Grain`, and the storage provider API, which is centered around `IGrainStorage` — the interface which storage providers must implement: +There are two parts to the state persistence APIs: the API exposed to the grain via `IPersistentState` or `Grain`, and the storage provider API, centered around `IGrainStorage`—the interface storage providers must implement: :::zone target="docs" pivot="orleans-7-0" @@ -443,7 +444,7 @@ public class InconsistentStateException : OrleansException } ``` -Any other failure conditions from a storage operation *must* cause the returned `Task` to be broken with an exception indicating the underlying storage issue. In many cases, this exception may be thrown back to the caller, which triggered the storage operation by calling a method on the grain. It's important to consider whether or not the caller will be able to deserialize this exception. For example, the client might not have loaded the specific persistence library containing the exception type. For this reason, it's advisable to convert exceptions into exceptions that can be propagated back to the caller. +Any other failure conditions from a storage operation *must* cause the returned `Task` to be broken with an exception indicating the underlying storage issue. In many cases, this exception might be thrown back to the caller who triggered the storage operation by calling a method on the grain. It's important to consider whether the caller can deserialize this exception. For example, the client might not have loaded the specific persistence library containing the exception type. For this reason, it's advisable to convert exceptions into exceptions that can propagate back to the caller. ### Data mapping @@ -451,6 +452,6 @@ Individual storage providers should decide how best to store grain state – blo ### Register a storage provider -The Orleans runtime resolves a storage provider from the service provider () when a grain is created. The runtime resolves an instance of . If the storage provider is named, for example via the `[PersistentState(stateName, storageName)]` attribute, then a named instance of `IGrainStorage` will be resolved. +The Orleans runtime resolves a storage provider from the service provider () when a grain is created. The runtime resolves an instance of . If the storage provider is named (for example, via the `[PersistentState(stateName, storageName)]` attribute), then a named instance of `IGrainStorage` is resolved. -To register a named instance of `IGrainStorage`, use the extension method following the example of the [AzureTableGrainStorage provider here](https://github.com/dotnet/orleans/blob/af974d37864f85bfde5dc02f2f60bba997f2162d/src/Azure/Orleans.Persistence.AzureStorage/Hosting/AzureTableSiloBuilderExtensions.cs#L78). +To register a named instance of `IGrainStorage`, use the extension method, following the example of the [AzureTableGrainStorage provider here](https://github.com/dotnet/orleans/blob/af974d37864f85bfde5dc02f2f60bba997f2162d/src/Azure/Orleans.Persistence.AzureStorage/Hosting/AzureTableSiloBuilderExtensions.cs#L78). diff --git a/docs/orleans/grains/grain-persistence/relational-storage.md b/docs/orleans/grains/grain-persistence/relational-storage.md index 1d902511121c5..4fe6f25211d00 100644 --- a/docs/orleans/grains/grain-persistence/relational-storage.md +++ b/docs/orleans/grains/grain-persistence/relational-storage.md @@ -1,26 +1,26 @@ --- title: ADO.NET grain persistence description: Learn about ADO.NET grain persistence in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: how-to --- # ADO.NET grain persistence -Relational storage backend code in Orleans is built on generic ADO.NET functionality and is consequently database vendor agnostic. The Orleans data storage layout has been explained already in runtime tables. Setting up the connection strings is done as explained in [Orleans Configuration Guide](../../host/configuration-guide/index.md). +Relational storage backend code in Orleans builds on generic ADO.NET functionality and is consequently database vendor agnostic. Set up connection strings as explained in the [Orleans Configuration Guide](../../host/configuration-guide/index.md). -To make Orleans code function with a given relational database backend, the following is required: +To make Orleans code function with a given relational database backend, you need the following: -1. The appropriate ADO.NET library must be loaded into the process. This should be defined as usual, for example, via the [DbProviderFactories](../../../framework/data/adonet/obtaining-a-dbproviderfactory.md) element in the application configuration. -2. Configure the ADO.NET invariant via the `Invariant` property in the options. -3. The database needs to exist and be compatible with the code. This is done by running a vendor-specific database creation script. For more information, see [ADO.NET Configuration](../../host/configuration-guide/adonet-configuration.md). +1. Load the appropriate ADO.NET library into the process. Define this as usual, for example, via the [DbProviderFactories](../../../framework/data/adonet/obtaining-a-dbproviderfactory.md) element in the application configuration. +1. Configure the ADO.NET invariant via the `Invariant` property in the options. +1. Ensure the database exists and is compatible with the code. Do this by running a vendor-specific database creation script. For more information, see [ADO.NET Configuration](../../host/configuration-guide/adonet-configuration.md). -The ADO .NET grain storage provider allows you to store the grain state in relational databases. -Currently, the following databases are supported: +The ADO.NET grain storage provider allows you to store grain state in relational databases. Currently, the following databases are supported: -* SQL Server -* MySQL/MariaDB -* PostgreSQL -* Oracle +- SQL Server +- MySQL/MariaDB +- PostgreSQL +- Oracle First, install the base package: @@ -28,9 +28,9 @@ First, install the base package: Install-Package Microsoft.Orleans.Persistence.AdoNet ``` -Read the [ADO.NET configuration](../../host/configuration-guide/adonet-configuration.md) article for information on configuring your database, including the corresponding ADO.NET Invariant and the setup scripts. +Read the [ADO.NET configuration](../../host/configuration-guide/adonet-configuration.md) article for information on configuring your database, including the corresponding ADO.NET Invariant and setup scripts. -The following is an example of how to configure an ADO.NET storage provider via : +The following example shows how to configure an ADO.NET storage provider via : ```csharp var siloHostBuilder = new HostBuilder() @@ -45,8 +45,7 @@ var siloHostBuilder = new HostBuilder() }); ``` -Essentially, you only need to set the database-vendor-specific connection string and an -`Invariant` (see [ADO.NET Configuration](../../host/configuration-guide/adonet-configuration.md)) that identifies the vendor. You may also choose the format in which the data is saved, which may be either binary (default), JSON, or XML. While binary is the most compact option, it is opaque, and you will not be able to read or work with the data. JSON is the recommended option. +Essentially, you only need to set the database-vendor-specific connection string and an `Invariant` (see [ADO.NET Configuration](../../host/configuration-guide/adonet-configuration.md)) identifying the vendor. You can also choose the format for saving data: binary (default), JSON, or XML. While binary is the most compact option, it's opaque, and you won't be able to read or work with the data directly. JSON is the recommended option. You can set the following properties via : @@ -106,26 +105,26 @@ public class AdoNetGrainStorageOptions } ``` -The ADO.NET persistence has the functionality to version data and define arbitrary (de)serializers with arbitrary application rules and streaming, but currently, there is no method to expose it to application code. +The ADO.NET persistence provider can version data and define arbitrary (de)serializers with custom application rules and streaming, but currently, there's no method to expose this functionality directly to application code. ## ADO.NET persistence rationale -The principles for ADO.NET backed persistence storage are: +The principles for ADO.NET-backed persistence storage are: -1. Keep business-critical data safe and accessible while data, the format of data, and the code evolve. -2. Take advantage of vendor-specific and storage-specific functionality. +1. Keep business-critical data safe and accessible while data, data format, and code evolve. +1. Take advantage of vendor-specific and storage-specific functionality. -In practice, this means adhering to ADO.NET implementation goals, and some added implementation logic in ADO.NET-specific storage providers that allow evolving the shape of the data in the storage. +In practice, this means adhering to ADO.NET implementation goals and including some added implementation logic in ADO.NET-specific storage providers that allow the shape of the data in storage to evolve. In addition to the usual storage provider capabilities, the ADO.NET provider has built-in capability to: -1. Change storage data from one format to another (for example, from JSON to binary) when round-tripping state. -2. Shape the type to be saved or read from the storage in arbitrary ways. This allows the version of the state to evolve. -3. Stream data out of the database. +1. Change storage data from one format to another (e.g., from JSON to binary) when round-tripping state. +1. Shape the type to be saved or read from storage in arbitrary ways. This allows the state version to evolve. +1. Stream data out of the database. -Both `1.` and `2.` can be applied based on arbitrary decision parameters, such as *grain ID*, *grain type*, *payload data*. +You can apply both `1.` and `2.` based on arbitrary decision parameters, such as *grain ID*, *grain type*, or *payload data*. -This is the case so that you can choose a serialization format, for example, [Simple Binary Encoding (SBE)](https://github.com/real-logic/simple-binary-encoding) and implements and . The built-in serializers have been built using this method: +This allows you to choose a serialization format, for example, [Simple Binary Encoding (SBE)](https://github.com/real-logic/simple-binary-encoding), and implement and . The built-in serializers were built using this method: - - @@ -134,71 +133,62 @@ This is the case so that you can choose a serialization format, for example, [Si - - -When the serializers have been implemented, they need to be added to the property in . Here is an implementation of `IStorageSerializationPicker`. By default, `StorageSerializationPicker` will be used. An example of changing the data storage format or using serializers can be seen at [RelationalStorageTests](https://github.com/dotnet/orleans/blob/main/test/Extensions/TesterAdoNet/StorageTests/Relational/RelationalStorageTests.cs). +After implementing the serializers, add them to the property in . `StorageSerializationPicker` is the default implementation of `IStorageSerializationPicker`. You can see an example of changing the data storage format or using serializers in [RelationalStorageTests](https://github.com/dotnet/orleans/blob/main/test/Extensions/TesterAdoNet/StorageTests/Relational/RelationalStorageTests.cs). -Currently, there is no method to expose the serialization picker to the Orleans application as there is no method to access the framework-created `AdoNetGrainStorage`. +Currently, there's no method to expose the serialization picker to the Orleans application, as there's no way to access the framework-created `AdoNetGrainStorage` instance directly. -## Goals of the design +## Design goals -### 1. Allow the use of any backend that has an ADO.NET provider +### 1. Allow use of any backend with an ADO.NET provider This should cover the broadest possible set of backends available for .NET, which is a factor in on-premises installations. Some providers are listed at [ADO.NET overview](../../../framework/data/adonet/ado-net-overview.md), but not all are listed, such as [Teradata](https://downloads.teradata.com/download/connectivity/net-data-provider-for-teradata). -### 2. Maintain the potential to tune queries and database structure as appropriate, even while a deployment is running +### 2. Maintain potential to tune queries and database structure, even while a deployment is running -In many cases, the servers and databases are hosted by a third party in contractual relation with the client. It is not an unusual -situation to find a hosting environment that is virtualized, and where performance fluctuates due to unforeseen factors, such as noisy neighbors or faulty hardware. It may not be possible to alter and re-deploy either Orleans binaries (for contractual reasons) or even application binaries, but it is usually possible to tweak the database deployment parameters. Altering *standard components*, such as Orleans binaries, requires a lengthier procedure for optimizing in a given situation. +In many cases, third parties host servers and databases under contract. It's not unusual to find virtualized hosting environments where performance fluctuates due to unforeseen factors like noisy neighbors or faulty hardware. Altering and redeploying Orleans binaries (due to contractual reasons) or even application binaries might not be possible. However, tweaking database deployment parameters is usually possible. Altering *standard components*, such as Orleans binaries, requires a lengthier optimization procedure for a given situation. -### 3. Allow you to make use of vendor-specific and version-specific abilities +### 3. Allow use of vendor-specific and version-specific abilities -Vendors have implemented different extensions and features within their products. It's sensible to make use of these features when they're available. These are features such as [native UPSERT](https://www.postgresql.org/about/news/1636/) or [PipelineDB](https://github.com/pipelinedb/pipelinedb/blob/master/README.md) in PostgreSQL, and [PolyBase](/sql/relational-databases/polybase/get-started-with-polybase) or [natively compiled tables and stored procedures](/sql/relational-databases/in-memory-oltp/native-compilation-of-tables-and-stored-procedures) in SQL Server. +Vendors implement different extensions and features in their products. It's sensible to use these features when available. Examples include [native UPSERT](https://www.postgresql.org/about/news/1636/) or [PipelineDB](https://github.com/pipelinedb/pipelinedb/blob/master/README.md) in PostgreSQL, and [PolyBase](/sql/relational-databases/polybase/get-started-with-polybase) or [natively compiled tables and stored procedures](/sql/relational-databases/in-memory-oltp/native-compilation-of-tables-and-stored-procedures) in SQL Server. -### 4. Make it possible to optimize hardware resources +### 4. Enable optimization of hardware resources -When designing an application, it is often possible to anticipate which data needs to be inserted faster than other data, and which data could more likely be put into *cold storage*, which is cheaper (for example, splitting data between SSD and HDD). Additional considerations include the physical location of the data (some data could be more expensive (for example, SSD RAID viz HDD RAID), or more secured), or some other decision basis. Related to *point 3.*, some databases offer special partitioning schemes, such as SQL Server [Partitioned Tables and Indexes](/sql/relational-databases/partitions/partitioned-tables-and-indexes). +When designing an application, you can often anticipate which data needs faster insertion and which data is more suitable for cheaper *cold storage* (e.g., splitting data between SSD and HDD). Additional considerations include the physical location of data (some storage might be more expensive, like SSD RAID vs. HDD RAID, or more secure) or other decision factors. Related to *point 3*, some databases offer special partitioning schemes, such as SQL Server [Partitioned Tables and Indexes](/sql/relational-databases/partitions/partitioned-tables-and-indexes). -These principles apply throughout the application life-cycle. Considering that one of the principles of Orleans itself is high availability, it should be possible to adjust the storage system without interruption to the Orleans deployment, or it should be possible to adjust the queries according to data and other application parameters. An example of dynamic changes may be seen in Brian Harry's [blog post](https://devblogs.microsoft.com/bharry/a-bit-more-on-the-feb-3-and-4-incidents/): +These principles apply throughout the application lifecycle. Considering that one of Orleans' core principles is high availability, it should be possible to adjust the storage system without interrupting the Orleans deployment. It should also be possible to adjust queries based on data and other application parameters. Brian Harry's [blog post](https://devblogs.microsoft.com/bharry/a-bit-more-on-the-feb-3-and-4-incidents/) provides an example of dynamic changes: > When a table is small, it almost doesn't matter what the query plan is. When it's medium, an OK query plan is fine, but when it's huge (millions upon millions or billions of rows), even a slight variation in the query plan can kill you. For this reason, we hint our sensitive queries heavily. -### 5. No assumptions on what tools, libraries, or deployment processes are used in organizations +### 5. Make no assumptions about tools, libraries, or deployment processes -Many organizations have familiarity with a certain set of database tools, examples being [Dacpac](/sql/relational-databases/data-tier-applications/data-tier-applications) or [Red Gate](https://www.red-gate.com/). It may be that deploying a database requires either permission or a person, such as someone in a DBA role, to do it. Usually, this means also having the target database layout and a rough sketch of the queries the application will produce for use in estimating the load. There might be processes, perhaps influenced by industry standards, which mandate script-based deployment. Having the queries and database structures in an external script makes this possible. +Many organizations are familiar with specific database tools, such as [Dacpac](/sql/relational-databases/data-tier-applications/data-tier-applications) or [Redgate](https://www.red-gate.com/). Deploying a database might require permission or a specific person, like someone in a DBA role. Usually, this also means having the target database layout and a rough sketch of the queries the application produces to estimate the load. Processes, possibly influenced by industry standards, might mandate script-based deployment. Having queries and database structures in external scripts makes this possible. -### 6. Use the minimum set of interface functionality needed to load the ADO.NET libraries and functionality +### 6. Use the minimum interface functionality needed to load ADO.NET libraries and functionality -This is both fast and has less surface exposed to the ADO.NET library implementation discrepancies. +This approach is both fast and exposes less surface area to potential discrepancies in ADO.NET library implementations. ### 7. Make the design shardable -When it makes sense, for instance in a relational storage provider, make the design readily shardable. For instance, this means using no database-dependent data (for example, `IDENTITY`). Information that distinguishes row data should build on only data from the actual parameters. +When appropriate (e.g., in a relational storage provider), make the design readily shardable. For instance, this means avoiding database-dependent data like `IDENTITY` columns. Information distinguishing row data should rely only on data from the actual parameters. ### 8. Make the design easy to test -Creating a new backend should ideally be as easy as translating one of the existing deployment scripts into the SQL dialect of the backend you are trying to target, adding a new connection string to the tests (assuming default -parameters), checking to see if a given database is installed, and then running the tests against it. +Creating a new backend should ideally be as simple as translating an existing deployment script into the SQL dialect of the target backend, adding a new connection string to the tests (assuming default parameters), checking if the database is installed, and then running the tests against it. -### 9. Taking into account the previous points, make both porting scripts for new backends and modifying already-deployed backend scripts as transparent as possible +### 9. Considering the previous points, make porting scripts for new backends and modifying existing backend scripts as transparent as possible ## Realization of the goals -The Orleans framework does not know about deployment-specific hardware (which hardware may change during active deployment), the change of data during the deployment life-cycle, or certain vendor-specific features which are only usable in certain situations. For this reason, the interface between the database and Orleans should adhere to the minimum set of abstractions and rules to meet these goals, make it robust against misuse, and make it easy to test if needed. Runtime Tables, Cluster Management and the concrete [membership protocol implementation](https://github.com/dotnet/orleans/blob/main/src/Orleans.Core/SystemTargetInterfaces/IMembershipTable.cs). Also, the SQL Server implementation contain SQL Server edition-specific tuning. The interface contract between the database and Orleans is defined as follows: +The Orleans framework doesn't know about deployment-specific hardware (which might change during active deployment), data changes during the deployment lifecycle, or certain vendor-specific features usable only in specific situations. For this reason, the interface between the database and Orleans should adhere to the minimum set of abstractions and rules to meet these goals, ensure robustness against misuse, and facilitate testing. See [Cluster management](../../implementation/cluster-management.md) and the concrete [membership protocol implementation](https://github.com/dotnet/orleans/blob/main/src/Orleans.Core/SystemTargetInterfaces/IMembershipTable.cs). Also, the SQL Server implementation contains SQL Server edition-specific tuning. The interface contract between the database and Orleans is defined as follows: -1. The general idea is that data is read and written through Orleans-specific queries. - Orleans operates on column names and types when reading, and on parameter names and types when writing. -2. The implementations **must** preserve input and output names and types. Orleans uses these parameters to read query results by name and type. - Vendor-specific and deployment-specific tuning is allowed, and contributions are encouraged as long as the interface contract is maintained. -3. The implementation across vendor-specific scripts **should** preserve the constraint names. - This simplifies troubleshooting, by virtue of uniform naming across concrete implementations. -4. **Version** – or **ETag** in application code – for Orleans, this represents a unique version. - The type of its actual implementation is not important as long as it represents a unique version. In the implementation, Orleans code expects a signed 32-bit integer. -5. For the sake of being explicit and removing ambiguity, Orleans expects some queries to return either **TRUE as > 0** value - or **FALSE as = 0** value. That is, the number of affected or returned rows does not matter. If an error is raised or an exception is thrown, - the query **must** ensure the entire transaction is rolled back and may either return FALSE or propagate the exception. -6. Currently, all but one query are single-row inserts or updates (note, one could replace `UPDATE` queries with `INSERT`, provided the associated - `SELECT` queries performed the last write). +1. The general idea is that data is read and written through Orleans-specific queries. Orleans operates on column names and types when reading, and on parameter names and types when writing. +1. Implementations **must** preserve input and output names and types. Orleans uses these parameters to read query results by name and type. Vendor-specific and deployment-specific tuning is allowed, and contributions are encouraged as long as the interface contract is maintained. +1. Implementations across vendor-specific scripts **should** preserve constraint names. This simplifies troubleshooting through uniform naming across concrete implementations. +1. **Version** – or **ETag** in application code – represents a unique version for Orleans. The type of its actual implementation isn't important as long as it represents a unique version. In the implementation, Orleans code expects a signed 32-bit integer. +1. To be explicit and remove ambiguity, Orleans expects some queries to return either **TRUE as > 0** value or **FALSE as = 0** value. The number of affected or returned rows doesn't matter. If an error is raised or an exception is thrown, the query **must** ensure the entire transaction rolls back and can either return FALSE or propagate the exception. +1. Currently, all but one query are single-row inserts or updates (note: you could replace `UPDATE` queries with `INSERT`, provided the associated `SELECT` queries performed the last write). -Database engines support in-database programming. This is similar to the idea of loading an executable script and invoking it to execute database operations. In pseudocode it could be depicted as: +Database engines support in-database programming. This is similar to loading an executable script and invoking it to execute database operations. In pseudocode, it could be depicted as: ```csharp const int Param1 = 1; @@ -214,18 +204,15 @@ TExpected queryResult = These principles are also [included in the database scripts](../../host/configuration-guide/adonet-configuration.md). -## Some ideas on applying customized scripts +## Ideas for applying customized scripts -1. Alter scripts in `OrleansQuery` for grain persistence with `IF ELSE` - so that some state is saved using the default `INSERT`, while some grain states may use [memory optimized tables](/sql/relational-databases/in-memory-oltp/memory-optimized-tables). - The `SELECT` queries need to be altered accordingly. -2. The idea in `1.` can be used to take advantage of another deployment- or vendor-specific aspects, such as splitting data between `SSD` or `HDD`, putting some data in encrypted tables, - or perhaps inserting statistics data via SQL-Server-to-Hadoop, or even [linked servers](/sql/relational-databases/linked-servers/linked-servers-database-engine). +1. Alter scripts in `OrleansQuery` for grain persistence using `IF ELSE` so that some state saves using the default `INSERT`, while other grain states might use [memory-optimized tables](/sql/relational-databases/in-memory-oltp/memory-optimized-tables). Alter the `SELECT` queries accordingly. +1. Use the idea in `1.` to take advantage of other deployment- or vendor-specific aspects, such as splitting data between `SSD` and `HDD`, putting some data in encrypted tables, or perhaps inserting statistics data via SQL Server-to-Hadoop or even [linked servers](/sql/relational-databases/linked-servers/linked-servers-database-engine). -The altered scripts can be tested by running the Orleans test suite, or straight in the database using, for instance, [SQL Server Unit Test Project](/previous-versions/sql/sql-server-data-tools/jj851212(v=vs.103)). +You can test the altered scripts by running the Orleans test suite or directly in the database using, for instance, a [SQL Server Unit Test Project](/previous-versions/sql/sql-server-data-tools/jj851212(v=vs.103)). ## Guidelines for adding new ADO.NET providers 1. Add a new database setup script according to the [Realization of the goals](#realization-of-the-goals) section above. -2. Add the vendor ADO invariant name to and ADO.NET provider-specific data to [DbConstantsStore](https://github.com/dotnet/orleans/blob/main/src/AdoNet/Shared/Storage/DbConstantsStore.cs). These are (potentially) used in some query operations. for example, to select the correct statistics insert mode (i.e. the `UNION ALL` with or without `FROM DUAL`). -3. Orleans has comprehensive tests for all system stores: membership, reminders and statistics. Adding tests for the new database script is done by copy-pasting existing test classes and changing the ADO invariant name. Also, derive from [RelationalStorageForTesting](https://github.com/dotnet/orleans/blob/main/test/Extensions/TesterAdoNet/RelationalUtilities/RelationalStorageForTesting.cs) in order to define test functionality for the ADO invariant. +1. Add the vendor ADO invariant name to and ADO.NET provider-specific data to [DbConstantsStore](https://github.com/dotnet/orleans/blob/main/src/AdoNet/Shared/Storage/DbConstantsStore.cs). These are potentially used in some query operations, for example,, to select the correct statistics insert mode (i.e., `UNION ALL` with or without `FROM DUAL`). +1. Orleans has comprehensive tests for all system stores: membership, reminders, and statistics. Add tests for the new database script by copy-pasting existing test classes and changing the ADO invariant name. Also, derive from [RelationalStorageForTesting](https://github.com/dotnet/orleans/blob/main/test/Extensions/TesterAdoNet/RelationalUtilities/RelationalStorageForTesting.cs) to define test functionality for the ADO invariant. diff --git a/docs/orleans/grains/grain-placement.md b/docs/orleans/grains/grain-placement.md index 454362d2aacfa..97edb2eea4003 100644 --- a/docs/orleans/grains/grain-placement.md +++ b/docs/orleans/grains/grain-placement.md @@ -1,81 +1,82 @@ --- title: Grain placement -description: Learn about grain placements in .NET Orleans. -ms.date: 07/03/2024 +description: Learn about grain placement in .NET Orleans. +ms.date: 03/31/2025 +ms.topic: conceptual --- # Grain placement -Orleans ensures that when a grain call is made there is an instance of that grain available in memory on some server in the cluster to handle the request. If the grain is not currently active in the cluster, Orleans picks one of the servers to activate the grain on. This is called _grain placement_. Placement is also one way that load is balanced: even placement of busy grains helps to even the workload across the cluster. +Orleans ensures that when a grain call is made, an instance of that grain is available in memory on some server in the cluster to handle the request. If the grain isn't currently active in the cluster, Orleans picks a server to activate the grain on. This process is called _grain placement_. Placement is also one way Orleans balances load: placing busy grains evenly helps distribute the workload across the cluster. -The placement process in Orleans is fully configurable: developers can choose from a set of out-of-the-box placement policies such as random, prefer-local, and load-based, or custom logic can be configured. This allows for full flexibility in deciding where grains are created. For example, grains can be placed on a server close to resources which they need to operate on or close to other grains with which they communicate. By default, Orleans will pick a random compatible server. +The placement process in Orleans is fully configurable. Choose from out-of-the-box placement policies such as random, prefer-local, and load-based, or configure custom logic. This allows full flexibility in deciding where grains are created. For example, place grains on a server close to resources they need to operate on or close to other grains they communicate with. By default, Orleans picks a random compatible server. -The placement strategy that Orleans uses can be configured globally or per grain class. +Configure the placement strategy Orleans uses globally or per grain class. ## Random placement -A server is randomly selected from the compatible servers in the cluster. To configure this placement strategy, add the to a grain. +Orleans randomly selects a server from the compatible servers in the cluster. To configure this placement strategy, add the to the grain class. ## Local placement -If the local server is compatible, select the local server, otherwise select a random server. This placement strategy is configured by adding the to a grain. +If the local server is compatible, Orleans selects the local server; otherwise, it selects a random server. Configure this placement strategy by adding the to the grain class. ## Hash-based placement -Hash the grain id to a non-negative integer and modulo it with the number of compatible servers. Select the corresponding server from the list of compatible servers ordered by server address. Note that this is not guaranteed to remain stable as the cluster membership changes. Specifically, adding, removing, or restarting servers can alter the server selected for a given grain id. Because grains placed using this strategy are registered in the grain directory, this change in placement decision as membership changes typically doesn't have a noticeable effect. +Orleans hashes the grain ID to a non-negative integer and applies a modulo operation with the number of compatible servers. It then selects the corresponding server from the list of compatible servers ordered by server address. Note that this placement isn't guaranteed to remain stable as cluster membership changes. Specifically, adding, removing, or restarting servers can alter the server selected for a given grain ID. Because grains placed using this strategy register in the grain directory, this change in placement decision as membership changes typically doesn't have a noticeable effect. -This placement strategy is configured by adding the to a grain. +Configure this placement strategy by adding the to the grain class. ## Activation-count-based placement -This placement strategy intends to place new grain activations on the least heavily loaded server based on the number of recently busy grains. It includes a mechanism in which all servers periodically publish their total activation count to all other servers. The placement director then selects a server that is predicted to have the fewest activations by examining the most recently reported activation count and predicts the current activation count based on the recent activation count made by the placement director on the current server. The director selects several servers at random when making this prediction, to avoid multiple separate servers overloading the same server. By default, two servers are selected randomly, but this value is configurable via . +This placement strategy attempts to place new grain activations on the least heavily loaded server based on the number of recently busy grains. It includes a mechanism where all servers periodically publish their total activation count to all other servers. The placement director then selects a server predicted to have the fewest activations by examining the most recently reported activation count and predicting the current count based on recent activations made by the placement director on the current server. The director selects several servers randomly when making this prediction to avoid multiple separate servers overloading the same server. By default, two servers are selected randomly, but this value can be configured via . -This algorithm is based on the thesis [*The Power of Two Choices in Randomized Load Balancing* by Michael David Mitzenmacher](https://www.eecs.harvard.edu/~michaelm/postscripts/mythesis.pdf), and is also used in Nginx for distributed load balancing, as described in the article [*NGINX and the "Power of Two Choices" Load-Balancing Algorithm*](https://www.nginx.com/blog/nginx-power-of-two-choices-load-balancing-algorithm/). +This algorithm is based on the thesis [*The Power of Two Choices in Randomized Load Balancing* by Michael David Mitzenmacher](https://www.eecs.harvard.edu/~michaelm/postscripts/mythesis.pdf). It's also used in Nginx for distributed load balancing, as described in the article [*NGINX and the "Power of Two Choices" Load-Balancing Algorithm*](https://www.nginx.com/blog/nginx-power-of-two-choices-load-balancing-algorithm/). -This placement strategy is configured by adding the to a grain. +Configure this placement strategy by adding the to the grain class. ## Stateless worker placement -Stateless worker placement is a special placement strategy used by [*stateless worker* grains](../grains/stateless-worker-grains.md). This placement operates almost identically to except that each server can have multiple activations of the same grain and the grain is not registered in the grain directory since there's no need. +Stateless worker placement is a special placement strategy used by [*stateless worker* grains](stateless-worker-grains.md). This placement operates almost identically to `PreferLocalPlacement`, except each server can have multiple activations of the same grain, and the grain isn't registered in the grain directory since there's no need. -This placement strategy is configured by adding the to a grain. +Configure this placement strategy by adding the to the grain class. -## Silo-role based placement +## Silo-role-based placement -A deterministic placement strategy that places grains on silos with a specific role. This placement strategy is configured by adding the to a grain. +This is a deterministic placement strategy placing grains on silos with a specific role. Configure this placement strategy by adding the to the grain class. ## Resource-optimized placement -The resource-optimized placement strategy attempts to optimize cluster resources by balancing grain activations across silos based on available memory and CPU usage. It assigns weights to runtime statistics to prioritize different resources and calculates a normalized score for each silo. The silo with the lowest score is chosen for placing the upcoming activation. Normalization ensures that each property contributes proportionally to the overall score. Weights can be adjusted via the based on user-specific requirements and priorities for different resources. +The resource-optimized placement strategy attempts to optimize cluster resources by balancing grain activations across silos based on available memory and CPU usage. It assigns weights to runtime statistics to prioritize different resources and calculates a normalized score for each silo. The silo with the lowest score is chosen for placing the upcoming activation. Normalization ensures each property contributes proportionally to the overall score. Adjust weights via based on specific requirements and priorities for different resources. -Additionally, this placement strategy exposes an option to build a stronger *preference* to the local silo (*the one which got the request for making a new placement*) to be picked as the target for the activation. This is controlled via the `LocalSiloPreferenceMargin` property which is part of the options. +Additionally, this placement strategy exposes an option to build a stronger *preference* for the local silo (the one receiving the request to make a new placement) to be picked as the target for the activation. Control this via the `LocalSiloPreferenceMargin` property, part of the options. -Also, an [*online*](https://en.wikipedia.org/wiki/Online_algorithm), [*adaptive*](https://en.wikipedia.org/wiki/Adaptive_algorithm) algorithm provides a smoothing effect which avoids rapid signal drops by transforming it into a polynomial-like decay process. This is especially important for CPU usage, and overall contributes to avoiding resource saturation on the silos, especially newly joined once. +Also, an [*online*](https://en.wikipedia.org/wiki/Online_algorithm), [*adaptive*](https://en.wikipedia.org/wiki/Adaptive_algorithm) algorithm provides a smoothing effect avoiding rapid signal drops by transforming the signal into a polynomial-like decay process. This is especially important for CPU usage and contributes overall to avoiding resource saturation on silos, particularly newly joined ones. -This algorithm is based on: [*Resource-based placement with cooperative dual-mode Kalman filtering*](https://www.ledjonbehluli.com/posts/orleans_resource_placement_kalman/) +This algorithm is based on [*Resource-based placement with cooperative dual-mode Kalman filtering*](https://www.ledjonbehluli.com/posts/orleans_resource_placement_kalman/). -This placement strategy is configured by adding the to a grain. +Configure this placement strategy by adding the to the grain class. ## Choose a placement strategy -Choosing the appropriate grain placement strategy, beyond the defaults that Orleans provides, requires monitoring and developer evaluation. The choice of placement strategy should be based on the size and complexity of the app, workload characteristics, and deployment environment. +Choosing the appropriate grain placement strategy, beyond the defaults Orleans provides, requires monitoring and evaluation. The choice should be based on the app's size and complexity, workload characteristics, and deployment environment. -Random placement relies on the [Law of Large Numbers](https://en.wikipedia.org/wiki/Law_of_large_numbers), so it's usually a good default when there is an unpredictable load spread across a large number of grains (10,000 plus). +Random placement relies on the [Law of Large Numbers](https://en.wikipedia.org/wiki/Law_of_large_numbers), so it's usually a good default for unpredictable loads spread across many grains (10,000 or more). -Activation-count-based placement also has a random element to it, relying on the Power of Two Choices principle, which is a commonly used algorithm for distributed load balancing and is used in popular load balancers. Silos frequently publish run-time statistics to other silos in the cluster, including: +Activation-count-based placement also has a random element, relying on the Power of Two Choices principle. This is a commonly used algorithm for distributed load balancing and is used in popular load balancers. Silos frequently publish runtime statistics to other silos in the cluster, including: - Available memory, total physical memory, and memory usage. - CPU usage. - Total activation count and recent active activation count. - - A sliding window of activations that were active in the last few seconds, sometimes referred to as the activation working set. + - A sliding window of activations active in the last few seconds, sometimes referred to as the activation working set. -From these statistics, only the activation counts are currently used to determine the load on a given silo. +From these statistics, only activation counts are currently used to determine the load on a given silo. -Ultimately, you should experiment with different strategies and monitor performance metrics to determine the best fit. By selecting the right grain placement strategy, you can optimize the performance, scalability, and cost-effectiveness of your Orleans apps. +Ultimately, experiment with different strategies and monitor performance metrics to determine the best fit. Selecting the right grain placement strategy optimizes the performance, scalability, and cost-effectiveness of Orleans apps. ## Configure the default placement strategy -Orleans will use random placement unless the default is overridden. The default placement strategy can be overridden by registering an implementation of during configuration: +Orleans uses random placement unless the default is overridden. Override the default placement strategy by registering an implementation of during configuration: ```csharp siloBuilder.ConfigureServices(services => @@ -84,12 +85,11 @@ siloBuilder.ConfigureServices(services => ## Configure the placement strategy for a grain -The placement strategy for a grain type is configured by adding the appropriate attribute on the grain class. -The relevant attributes are specified in the [placement strategies](#random-placement) sections. +Configure the placement strategy for a grain type by adding the appropriate attribute to the grain class. Relevant attributes are specified in the [placement strategies](#random-placement) sections above. ## Sample custom placement strategy -First define a class that implements interface, requiring a single method. In this example, we assume you have a function `GetSiloNumber` defined which will return a silo number given the of the grain about to be created. +First, define a class implementing the interface, requiring a single method. In this example, assume a function `GetSiloNumber` is defined that returns a silo number given the of the grain about to be created. ```csharp public class SamplePlacementStrategyFixedSiloDirector : IPlacementDirector @@ -107,7 +107,7 @@ public class SamplePlacementStrategyFixedSiloDirector : IPlacementDirector } ``` -You then need to define two classes to allow grain classes to be assigned to the strategy: +Then, define two classes to allow assigning grain classes to the strategy: ```csharp [Serializable] @@ -125,7 +125,7 @@ public sealed class SamplePlacementStrategyAttribute : PlacementAttribute } ``` -Then just tag any grain classes you want to use this strategy with the attribute: +Then, tag any grain classes intended to use this strategy with the attribute: ```csharp [SamplePlacementStrategy] @@ -135,7 +135,7 @@ public class MyGrain : Grain, IMyGrain } ``` -And finally, register the strategy when you build the : +Finally, register the strategy when building the `ISiloHost`: ```csharp private static async Task StartSilo() @@ -158,4 +158,4 @@ private static void ConfigureServices(IServiceCollection services) } ``` -For a second simple example showing further use of the placement context, refer to the `PreferLocalPlacementDirector` in the [Orleans source repo](https://github.com/dotnet/orleans/blob/main/src/Orleans.Runtime/Placement/PreferLocalPlacementDirector.cs). +For a second simple example showing further use of the placement context, refer to `PreferLocalPlacementDirector` in the [Orleans source repository](https://github.com/dotnet/orleans/blob/main/src/Orleans.Runtime/Placement/PreferLocalPlacementDirector.cs). diff --git a/docs/orleans/grains/grain-references.md b/docs/orleans/grains/grain-references.md index e74f66531e232..5443606b45998 100644 --- a/docs/orleans/grains/grain-references.md +++ b/docs/orleans/grains/grain-references.md @@ -1,18 +1,19 @@ --- title: Grain references description: Learn about grain references in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Grain references -Before calling a method on grain, you first need a reference to that grain. A grain reference is a proxy object that implements the same grain interface as the corresponding grain class. It encapsulates the logical identity (type and unique key) of the target grain. Grain references are used for making calls to the target grain. Each grain reference is to a single grain (a single instance of the grain class), but one can create multiple independent references to the same grain. +Before calling a method on a grain, you first need a reference to that grain. A grain reference is a proxy object implementing the same grain interface as the corresponding grain class. It encapsulates the logical identity (type and unique key) of the target grain. You use grain references to make calls to the target grain. Each grain reference points to a single grain (a single instance of the grain class), but you can create multiple independent references to the same grain. -Since a grain reference represents the logical identity of the target grain, it is independent of the physical location of the grain, and stays valid even after a complete restart of the system. Developers can use grain references like any other .NET object. It can be passed to a method, used as a method return value, and even saved to persistent storage. +Since a grain reference represents the logical identity of the target grain, it's independent of the grain's physical location and remains valid even after a complete system restart. You can use grain references like any other .NET object. You can pass it to a method, use it as a method return value, and even save it to persistent storage. -A grain reference can be obtained by passing the identity of a grain to the method, where `T` is the grain interface and `key` is the unique key of the grain within the type. +You can obtain a grain reference by passing the identity of a grain to the method, where `T` is the grain interface and `key` is the unique key of the grain within its type. -The following are examples of how to obtain a grain reference of the `IPlayerGrain` interface defined above. +The following examples show how to obtain a grain reference for the `IPlayerGrain` interface defined previously. From within a grain class: @@ -33,25 +34,25 @@ IPlayerGrain player = client.GetGrain(playerId); Grain references contain three pieces of information: 1. The grain _type_, which uniquely identifies the grain class. -2. The grain _key_, which uniquely identifies a logical instance of that grain class. -3. The _interface_ which the grain reference must implement. +1. The grain _key_, which uniquely identifies a logical instance of that grain class. +1. The _interface_ which the grain reference must implement. > [!NOTE] > The grain _type_ and _key_ form the [grain identity](grain-identity.md). -Notice that the above calls to accepted only two of those three things: +Notice that the preceding calls to accepted only two of these three things: -* The _interface_ implemented by the grain reference, `IPlayerGrain`. -* The grain _key_, which is the value of `playerId`. +- The _interface_ implemented by the grain reference, `IPlayerGrain`. +- The grain _key_, which is the value of `playerId`. -Despite stating that a grain reference contains a grain _type_, _key_, and _interface_, the examples only provided Orleans with the _key_ and _interface_. That is because Orleans maintains a mapping between grain interfaces and grain types. When you ask the grain factory for `IShoppingCartGrain`, Orleans consults its mapping to find the corresponding grain type so that it can create the reference. This works when there's only one implementation of a grain interface, but if there are multiple implementations, then you will need to disambiguate them in the `GetGrain` call. For more information, see the next section, [disambiguating grain type resolution](#disambiguating-grain-type-resolution). +Despite stating that a grain reference contains a grain _type_, _key_, and _interface_, the examples only provided Orleans with the _key_ and _interface_. This is because Orleans maintains a mapping between grain interfaces and grain types. When you ask the grain factory for `IShoppingCartGrain`, Orleans consults its mapping to find the corresponding grain type so it can create the reference. This works when there's only one implementation of a grain interface. However, if there are multiple implementations, you need to disambiguate them in the `GetGrain` call. For more information, see the next section, [Disambiguating grain type resolution](#disambiguating-grain-type-resolution). > [!NOTE] -> Orleans generates grain reference implementation types for each grain interface in your application during compilation. These grain reference implementations inherit from the class. `GetGrain` returns instances of the generated implementation corresponding to the requested grain interface. +> Orleans generates grain reference implementation types for each grain interface in your application during compilation. These grain reference implementations inherit from the class. `GetGrain` returns instances of the generated implementation corresponding to the requested grain interface. ## Disambiguating grain type resolution -When there are multiple implementations of a grain interface, such as in the following example, Orleans attempts to determine the intended implementation when creating a grain reference. Consider the following example, in which there are two implementations of the `ICounterGrain` interface: +When multiple implementations of a grain interface exist, such as in the following example, Orleans attempts to determine the intended implementation when creating a grain reference. Consider the following example, where there are two implementations of the `ICounterGrain` interface: ```csharp public interface ICounterGrain : IGrainWithStringKey @@ -74,20 +75,20 @@ public class DownCounterGrain : ICounterGrain } ``` -The following call to `GetGrain` will throw an exception because Orleans doesn't know how to unambiguously map `ICounterGrain` to one of the grain classes. +The following call to `GetGrain` throws an exception because Orleans doesn't know how to unambiguously map `ICounterGrain` to one of the grain classes. ```csharp // This will throw an exception: there is no unambiguous mapping from ICounterGrain to a grain class. ICounterGrain myCounter = grainFactory.GetGrain("my-counter"); ``` -An will thrown with the following message: +An is thrown with the following message: ```Output Unable to identify a single appropriate grain type for interface ICounterGrain. Candidates: upcounter (UpCounterGrain), downcounter (DownCounterGrain) ``` -The error message tells you which grain implementation's Orleans has which match the requested grain interface type, `ICounterGrain`. It shows you the grain type names (`upcounter` and `downcounter`) as well as the grain classes (`UpCounterGrain` and `DownCounterGrain`). +The error message tells you which grain implementations Orleans found that match the requested grain interface type, `ICounterGrain`. It shows the grain type names (`upcounter` and `downcounter`) and the grain classes (`UpCounterGrain` and `DownCounterGrain`). > [!NOTE] > The grain type names in the preceding error message, `upcounter` and `downcounter`, are derived from the grain class names, `UpCounterGrain` and `DownCounterGrain` respectively. This is the default behavior in Orleans and it can be customized by adding a `[GrainType(string)]` attribute to the grain class. For example: @@ -97,11 +98,11 @@ The error message tells you which grain implementation's Orleans has which match > public class UpCounterGrain : IUpCounterGrain { /* as above */ } > ``` -There are several ways to resolve this ambiguity detailed in the following subsections. +There are several ways to resolve this ambiguity, detailed in the following subsections. ### Disambiguating grain types using unique marker interfaces -The clearest way to disambiguate these grains is to give them unique grain interfaces. For example, if we add the interface `IUpCounterGrain` to the `UpCounterGrain` class and add the interface `IDownCounterGrain` to the `DownCounterGrain` class, like in the following example, then we can resolve the correct grain reference by passing `IUpCounterGrain` or `IDownCounterGrain` to the `GetGrain` call instead of passing the ambiguous `ICounterGrain` type. +The clearest way to disambiguate these grains is to give them unique grain interfaces. For example, if you add the interface `IUpCounterGrain` to the `UpCounterGrain` class and add the interface `IDownCounterGrain` to the `DownCounterGrain` class, as in the following example, you can resolve the correct grain reference by passing `IUpCounterGrain` or `IDownCounterGrain` to the `GetGrain` call instead of the ambiguous `ICounterGrain` type. ```csharp public interface ICounterGrain : IGrainWithStringKey @@ -139,7 +140,7 @@ ICounterGrain myDownCounter = grainFactory.GetGrain("my-count ``` > [!NOTE] -> In the preceding example, you created two grain references with the same key, but different grain types. The first, stored in the `myUpCounter` variable, is a reference to the grain with the id `upcounter/my-counter`. The second, stored in the `myDownCounter` variable, is a reference to the grain with the id `downcounter/my-counter`. It's the combination of grain _type_ and grain _key_ which uniquely identify a grain. Therefore, `myUpCounter` and `myDownCounter` refer to different grains. +> In the preceding example, you created two grain references with the same key but different grain types. The first, stored in the `myUpCounter` variable, references the grain with the ID `upcounter/my-counter`. The second, stored in the `myDownCounter` variable, references the grain with the ID `downcounter/my-counter`. The combination of grain _type_ and grain _key_ uniquely identifies a grain. Therefore, `myUpCounter` and `myDownCounter` refer to different grains. #### Disambiguating grain types by providing a grain class prefix @@ -152,7 +153,7 @@ ICounterGrain myDownCounter = grainFactory.GetGrain("my-counter", ### Specifying the default grain implementation using the naming convention -When disambiguating multiple implementations of the same grain interface, Orleans will select an implementation using the convention of stripping a leading 'I' from the interface name. For example, if the interface name is `ICounterGrain` and there are two implementations, `CounterGrain` and `DownCounterGrain`, Orleans will chose `CounterGrain` when asked for a reference to `ICounterGrain`, as in the following example: +When disambiguating multiple implementations of the same grain interface, Orleans selects an implementation using the convention of stripping a leading 'I' from the interface name. For example, if the interface name is `ICounterGrain` and there are two implementations, `CounterGrain` and `DownCounterGrain`, Orleans chooses `CounterGrain` when asked for a reference to `ICounterGrain`, as in the following example: ```csharp /// This will refer to an instance of CounterGrain, since that matches the convention. @@ -161,7 +162,7 @@ ICounterGrain myUpCounter = grainFactory.GetGrain("my-counter"); ### Specifying the default grain type using an attribute -The attribute can be added to a grain interface to specify the grain type of the default implementation for that interface, as in the following example: +You can add the attribute to a grain interface to specify the grain type of the default implementation for that interface, as shown in the following example: ```csharp [DefaultGrainType("up-counter")] @@ -192,9 +193,9 @@ public class DownCounterGrain : ICounterGrain ICounterGrain myUpCounter = grainFactory.GetGrain("my-counter"); ``` -### Disambiguating grain types by providing the resolved grain id +### Disambiguating grain types by providing the resolved grain ID -Some overloads of accept an argument of type . When using these overloads, Orleans doesn't need to map from an interface type to a grain type and therefore there is no ambiguity to be resolved. For example: +Some overloads of accept an argument of type . When using these overloads, Orleans doesn't need to map from an interface type to a grain type, so there's no ambiguity to resolve. For example: ```csharp public interface ICounterGrain : IGrainWithStringKey diff --git a/docs/orleans/grains/grain-versioning/backward-compatibility-guidelines.md b/docs/orleans/grains/grain-versioning/backward-compatibility-guidelines.md index 2f2a669b55fc3..22e559cdcbbfd 100644 --- a/docs/orleans/grains/grain-versioning/backward-compatibility-guidelines.md +++ b/docs/orleans/grains/grain-versioning/backward-compatibility-guidelines.md @@ -1,17 +1,17 @@ --- title: Backward compatibility guidelines description: Learn the backward compatibility guidelines in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Backward compatibility guidelines -Writing backward compatible code can be hard and difficult to test. This article discusses the guidelines for writing backward compatible code in .NET Orleans. This article covers the usage of , and . +Writing backward-compatible code can be hard and difficult to test. This article discusses guidelines for writing backward-compatible code in .NET Orleans. It covers the usage of and . ## Never change the signature of existing methods -Because of how the Orleans serializer works, you should never change the signature -of existing methods. +Because of how the Orleans serializer works, you should never change the signature of existing methods. The following example is correct: @@ -36,7 +36,7 @@ public interface IMyGrain : IGrainWithIntegerKey } ``` -This is not correct: +This example is incorrect: ```csharp [Version(1)] @@ -79,20 +79,18 @@ public interface IMyGrain : IGrainWithIntegerKey } ``` -These methods seem identical. But if the client was called with V1, and the request is -handled by a V2 activation: +These methods seem identical. However, if the client calls with V1, and a V2 activation handles the request: ```csharp var grain = client.GetGrain(0); var result = await grain.Subtract(5, 4); // Will return "-1" instead of expected "1" ``` -This is due to how the internal Orleans serializer works. +This happens due to how the internal Orleans serializer works. ## Avoid changing existing method logic -It can seem obvious, but you should be very careful when changing the body of an existing method. -Unless you are fixing a bug, it is better to just add a new method if you need to modify the code. +It might seem obvious, but be very careful when changing the body of an existing method. Unless you're fixing a bug, it's better to add a new method if you need to modify the code. Example: @@ -130,10 +128,9 @@ public interface MyGrain : IMyGrain ## Do not remove methods from grain interfaces -Unless you are sure that they're no longer used, you should not remove methods from the grain interface. -If you want to remove methods, this should be done in 2 steps: +Unless you're sure they're no longer used, don't remove methods from the grain interface. If you want to remove methods, do it in two steps: -1. Deploy V2 grains, with V1 method marked as `Obsolete` +1. Deploy V2 grains, with the V1 method marked as `Obsolete`. ```csharp [Version(1)] @@ -157,7 +154,7 @@ If you want to remove methods, this should be done in 2 steps: } ``` -2. When you are sure that no V1 calls are made (effectively V1 is no longer deployed in the running cluster), deploy V3 with V1 method removed. +1. When you're sure no V1 calls are being made (effectively, V1 is no longer deployed in the running cluster), deploy V3 with the V1 method removed. ```csharp [Version(3)] diff --git a/docs/orleans/grains/grain-versioning/compatible-grains.md b/docs/orleans/grains/grain-versioning/compatible-grains.md index a45d12bd6a63a..b9d06a7513bd8 100644 --- a/docs/orleans/grains/grain-versioning/compatible-grains.md +++ b/docs/orleans/grains/grain-versioning/compatible-grains.md @@ -1,52 +1,47 @@ --- title: Compatible grains description: Learn about compatible grains in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Compatible grains -When an existing grain activation is about to process a request, the runtime will check if the version in the request and the actual version of the grain are compatible. Orleans does not infer at runtime which policy to use. The default behavior to determine if two versions are compatible is determined by . +When an existing grain activation is about to process a request, the runtime checks if the version in the request and the actual version of the grain are compatible. Orleans doesn't infer at runtime which policy to use. The default behavior for determining compatibility between two versions is defined by . ## Backward compatible (default) ### Definition -A grain interface version Vn can be backward compatible with Vm if: +A grain interface version `Vn` can be backward compatible with `Vm` if: -- The name of the interface didn't change (or the overridden typecode). -- All public methods present in the Vm version are in the Vn version. - **It is important that the signatures of the methods inherited from Vm are not modified**: since Orleans use - an internal built-in serializer, modifying/renaming a field (even private) can make the - serialization to break. +- The name of the interface didn't change (or the overridden type code). +- All public methods present in the `Vm` version are also present in the `Vn` version. + **It's important not to modify the signatures of methods inherited from `Vm`**: since Orleans uses an internal built-in serializer, modifying or renaming a field (even private) can break serialization. -Since Vn can have added methods compared to Vm, Vm is not compatible with Vn. +Since `Vn` can have added methods compared to `Vm`, `Vm` is not compatible with `Vn`. ### Example -If in the cluster we have two versions of a given interface, V1 and V2 and that V2 is backward compatible -with V1: +If you have two versions of a given interface in the cluster, V1 and V2, and V2 is backward compatible with V1: -- If the current activation is a V2 and the requested version is V1, the current activation will - be able to process the request normally -- If the current activation is a V1 and the requested version is V2, the current activation will be - deactivated and a new activation compatible with V2 will be created (see [version selector strategy](version-selector-strategy.md)). +- If the current activation is V2 and the requested version is V1, the current activation can process the request normally. +- If the current activation is V1 and the requested version is V2, Orleans deactivates the current activation and creates a new activation compatible with V2 (see [Version selector strategy](version-selector-strategy.md)). ## Fully compatible ### Definition -A grain interface version Vn can be fully compatible with Vm if: +A grain interface version `Vn` can be fully compatible with `Vm` if: -- Vn is backward compatible with Vm -- No public methods were added in the Vn version +- `Vn` is backward compatible with `Vm`. +- No public methods were added in the `Vn` version. -If Vn is fully compatible with Vm then Vm is also fully compatible with Vn. +If `Vn` is fully compatible with `Vm`, then `Vm` is also fully compatible with `Vn`. ### Example -If in the cluster we have two versions of a given interface, V1 and V2 and that V2 is fully compatible -with V1: +If you have two versions of a given interface in the cluster, V1 and V2, and V2 is fully compatible with V1: -- If the current activation is a V2 and the requested version is V1, the current activation will be able to process the request normally -- If the current activation is a V1 and the requested version is V2, the current activation will also be able to process the request normally +- If the current activation is V2 and the requested version is V1, the current activation can process the request normally. +- If the current activation is V1 and the requested version is V2, the current activation can also process the request normally. diff --git a/docs/orleans/grains/grain-versioning/deploying-new-versions-of-grains.md b/docs/orleans/grains/grain-versioning/deploying-new-versions-of-grains.md index 0a0a2a99b9a0d..7082a5d59c318 100644 --- a/docs/orleans/grains/grain-versioning/deploying-new-versions-of-grains.md +++ b/docs/orleans/grains/grain-versioning/deploying-new-versions-of-grains.md @@ -1,16 +1,17 @@ --- title: Deploy new version of grains -description: Learn how to deploy new version of grains in .NET Orleans. -ms.date: 07/03/2024 +description: Learn how to deploy new versions of grains in .NET Orleans. +ms.date: 05/23/2025 +ms.topic: how-to --- -# Deploy new version of grains +# Deploy new versions of grains -In this article, you'll learn how to deploy new versions of grains in .NET Orleans. +In this article, you learn how to deploy new versions of grains in .NET Orleans. -### Rolling upgrade +## Rolling upgrade -With the rolling upgrade methodology, you deploy newer silos directly on your environment. This is the simplest method, but it can be difficult to interrupt an ongoing deployment and rollback. +With the rolling upgrade methodology, you deploy newer silos directly into your environment. This is the simplest method, but interrupting an ongoing deployment and rolling back can be difficult. Recommended configuration: @@ -30,15 +31,13 @@ var silo = new HostBuilder() .Build(); ``` -When using this configuration, "old" clients will be able to talk to activations -on both versions of silos. Newer clients and silos will only trigger new activations -on newer silos. +When using this configuration, "old" clients can talk to activations on both versions of silos. Newer clients and silos only trigger new activations on newer silos. ![Rolling gif](rolling.gif) -### Use a staging environment +## Use a staging environment -With the staging environment methodology, you will need a second environment (Staging environment), on which you will deploy newer silos before stopping the Production environment. The Production and the Staging silos and clients will be **part of the same cluster**. Silos from both environments must have the ability to talk to each other. +With the staging environment methodology, you need a second environment (staging environment) where you deploy newer silos before stopping the production environment. The production and staging silos and clients are **part of the same cluster**. Silos from both environments must be able to communicate with each other. Recommended configuration: @@ -60,18 +59,11 @@ var silo = new HostBuilder() Suggested deployment steps: -1. "V1" silos and clients are deployed and are running in the Production slot. -2. "V2" silos and clients begin to start in the Staging slot. They will join the -same cluster as the Production slot. No "V2" activations will be created so far. -3. Once the deployment in the Staging slot is finished, the developer can redirect -some traffic to the V2 clients (smoke tests, targeted beta users, etc.). This will -create V2 activations, but since Grains are backward compatible and all silos -are in the same cluster, no duplicate activations will be created. -4. If the validation is successful, proceed to VIP swap. - If not, you can safely shut down the Staging cluster: existing V2 activations will be - destroyed and V1 activations will be created if needed. -5. V1 activations will naturally "migrate" to V2 silos eventually. You can safely shutdown -V1 silos. +1. "V1" silos and clients are deployed and running in the production slot. +1. "V2" silos and clients begin starting in the staging slot. They join the same cluster as the production slot. No "V2" activations are created yet. +1. Once the deployment in the staging slot finishes, you can redirect some traffic to the V2 clients (for smoke tests, targeted beta users, etc.). This creates V2 activations. Since grains are backward compatible and all silos are in the same cluster, no duplicate activations are created. +1. If validation is successful, proceed to VIP swap. If not, you can safely shut down the staging cluster: existing V2 activations are destroyed, and V1 activations are created if needed. +1. V1 activations naturally "migrate" to V2 silos eventually. You can safely shut down the V1 silos. > [!WARNING] -> Remember that stateless workers are not versioned and that streaming agents will also start in the staging environment. +> Remember that stateless workers aren't versioned and that streaming agents also start in the staging environment. diff --git a/docs/orleans/grains/grain-versioning/grain-versioning.md b/docs/orleans/grains/grain-versioning/grain-versioning.md index 1f58d3f8d96d1..0fe24a91d21cb 100644 --- a/docs/orleans/grains/grain-versioning/grain-versioning.md +++ b/docs/orleans/grains/grain-versioning/grain-versioning.md @@ -1,12 +1,13 @@ --- title: Grain interface versioning description: Learn how to use grain interface versioning in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Grain interface versioning -In this article, you'll learn how to use grain interface versioning. The versioning of Grain state is out of scope. +In this article, you learn how to use grain interface versioning. The versioning of grain state is out of scope. ## Overview @@ -18,12 +19,12 @@ In this example, the client and Silo{1,2,3} were compiled with grain interface ` ## Limitations -- No versioning on stateless worker -- Streaming interfaces are not versioned +- No versioning on stateless workers. +- Streaming interfaces aren't versioned. ## Enable versioning -If the version attribute is not explicitly added to the grain interface, then the grains have a default version of 0. You can version grain by using the VersionAttribute on the grain interface: +If you don't explicitly add the version attribute to the grain interface, the grain has a default version of 0. You can version a grain by using the `VersionAttribute` on the grain interface: ```csharp [Version(X)] @@ -38,17 +39,17 @@ Where `X` is the version number of the grain interface, which is typically monot When a call from a versioned grain arrives in a cluster: -- If no activation exists, a compatible activation will be created +- If no activation exists, Orleans creates a compatible activation. - If an activation exists: - - If the current one is not compatible, it will be deactivated and new compatible one will be created (see [version selector strategy](version-selector-strategy.md)) - - If the current one is compatible (see [compatible grains](compatible-grains.md)), the call will be handled normally. + - If the current activation isn't compatible, Orleans deactivates it and creates a new compatible one (see [Version selector strategy](version-selector-strategy.md)). + - If the current activation is compatible (see [Compatible grains](compatible-grains.md)), Orleans handles the call normally. By default: -- All versioned grains are supposed to be backward-compatible only (see [backward compatibility guidelines](backward-compatibility-guidelines.md) and [compatible grains](compatible-grains.md)). That means that a v1 grain can make calls to a v2 grain, but a v2 grain cannot call a v1. -- When multiple versions exist in the cluster, the new activation will be randomly placed on a compatible silo. +- All versioned grains are assumed to be backward-compatible only (see [Backward compatibility guidelines](backward-compatibility-guidelines.md) and [Compatible grains](compatible-grains.md)). This means a v1 grain can make calls to a v2 grain, but a v2 grain cannot call a v1 grain. +- When multiple versions exist in the cluster, Orleans randomly places the new activation on a compatible silo. -You can change this default behavior via the : +You can change this default behavior via : ```csharp var silo = new HostBuilder() diff --git a/docs/orleans/grains/grain-versioning/version-selector-strategy.md b/docs/orleans/grains/grain-versioning/version-selector-strategy.md index 8c8f5afef8deb..03d50eb825b8e 100644 --- a/docs/orleans/grains/grain-versioning/version-selector-strategy.md +++ b/docs/orleans/grains/grain-versioning/version-selector-strategy.md @@ -1,35 +1,35 @@ --- title: Version selector strategy description: Learn how to use the version selector strategy in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Version selector strategy -When several versions of the same grain interface exist in the cluster, and a new activation has to be created, a [compatible version](compatible-grains.md) will be chosen according to the strategy defined in . +When several versions of the same grain interface exist in the cluster and a new activation needs to be created, Orleans chooses a [compatible version](compatible-grains.md) according to the strategy defined in . -Orleans out of the box supports the following strategies: +Orleans supports the following strategies out of the box: ## All compatible versions (default) -Using this strategy, the version of the new activation will be chosen randomly -across all compatible versions. +Using this strategy, Orleans chooses the version of the new activation randomly from all compatible versions. -For example, if we have 2 versions of a given grain interface, V1 and V2: +For example, if you have two versions of a given grain interface, V1 and V2: -- V2 is backward compatible with V1 -- In the cluster, 2 silos support V2, 8 support V1 -- The request was made from a V1 client/silo +- V2 is backward compatible with V1. +- In the cluster, 2 silos support V2, and 8 support V1. +- The request was made from a V1 client/silo. -In this case, there is a 20% chance that the new activation will be a V2 and an 80% chance that it will be a V1. +In this case, there's a 20% chance the new activation will be V2 and an 80% chance it will be V1. ## Latest version -Using this strategy, the version of the new activation will always be the latest compatible version. For example, if we have 2 versions of a given grain interface, V1 and V2 (V2 is backward or fully compatible with V1) then all new activations will be V2. +Using this strategy, the version of the new activation is always the latest compatible version. For example, if you have two versions of a given grain interface, V1 and V2 (where V2 is backward or fully compatible with V1), then all new activations will be V2. ## Minimum version -Using this strategy, the version of the new activation will always be the requested or the minimum compatible version. For example, if we have 2 versions of a given grain interface, V2, V3, all fully compatibles: +Using this strategy, the version of the new activation is always the requested version or the minimum compatible version. For example, if you have two versions of a given grain interface, V2 and V3, both fully compatible: -- If the request was made from a V1 client/silo, the new activation will be a V2 -- If the request was made from a V3 client/silo, the new activation will be a V2 too +- If the request was made from a V1 client/silo, the new activation will be V2. +- If the request was made from a V3 client/silo, the new activation will also be V2. diff --git a/docs/orleans/grains/grainservices.md b/docs/orleans/grains/grainservices.md index 56307b09c50f2..9d48ba257c914 100644 --- a/docs/orleans/grains/grainservices.md +++ b/docs/orleans/grains/grainservices.md @@ -1,23 +1,24 @@ --- title: Create a GrainService description: Learn how to create a GrainService in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual zone_pivot_groups: orleans-version --- -# Grain Services +# Grain services -Grain Services are remotely accessible, partitioned services for supporting the functionality grains. Each instance of a grain service is responsible for some set of grains and those grains can get a reference to the grain service which is currently responsible for servicing them by using a `GrainServiceClient`. +Grain services are remotely accessible, partitioned services for supporting grain functionality. Each instance of a grain service is responsible for some set of grains. Those grains can get a reference to the grain service currently responsible for servicing them by using a `GrainServiceClient`. -Grain Services exist to support cases where responsibility for servicing grains should be distributed around the Orleans cluster. For example, Orleans Reminders are implemented using grain services: each silo is responsible for handling reminder operations for a subset of grains and notifying those grains when their reminders fire. +Grain services exist to support cases where responsibility for servicing grains should be distributed around the Orleans cluster. For example, Orleans Reminders are implemented using grain services: each silo handles reminder operations for a subset of grains and notifies those grains when their reminders fire. -Grain Services are configured on silos and are initialized when the silo starts, before the silo completes initialization. They are not collected when idle and instead have lifetimes which extend for the lifetime of the silo itself. +You configure grain services on silos. They initialize when the silo starts, before the silo completes initialization. They aren't collected when idle; instead, their lifetimes extend for the lifetime of the silo itself. -## Create a GrainService +## Create a grain service -A is a special grain; one that has no stable identity, and runs in every silo from startup to shutdown. There are several steps involved when implementing an interface. +A is a special grain: it has no stable identity and runs in every silo from startup to shutdown. Implementing an interface involves several steps. -1. Define the grain service communication interface. The interface of a `GrainService` is built using the same principles you would use for building the interface of a grain. +1. Define the grain service communication interface. Build the interface of a `GrainService` using the same principles you use for building a grain interface. ```csharp public interface IDataService : IGrainService @@ -26,7 +27,7 @@ A is a special grain; one that has no stable } ``` -1. Create the `DataService` grain service. It's good to know that you can also inject an so you can make grain calls from your `GrainService`. +1. Create the `DataService` grain service. It's helpful to know that you can also inject an so you can make grain calls from your `GrainService`. :::zone target="docs" pivot="orleans-7-0" @@ -104,7 +105,7 @@ A is a special grain; one that has no stable :::zone-end -1. Create an interface for the `GrainServiceClient` to be used by other grains to connect to the `GrainService`. +1. Create an interface for the `GrainServiceClient` that other grains will use to connect to the `GrainService`. ```csharp public interface IDataServiceClient : IGrainServiceClient, IDataService @@ -112,10 +113,7 @@ A is a special grain; one that has no stable } ``` - -:::zone target="docs" pivot="orleans-7-0" - -1. Create the grain service client. Clients typically act as proxies for the grain services which they target, so you will usually add a method for each method on the target service. These methods will need to get a reference to the grain service which they target so that they can call into it. The `GrainServiceClient` base class provides several overloads of the `GetGrainService` method which can return a grain reference corresponding to a `GrainId`, a numeric hash (`uint`), or a `SiloAddress`. The latter two overloads are for advanced cases where a developer wants to use a different mechanism to map responsibility to hosts or wants to address a host directly. In our sample code below, we define a property, `GrainService`, which returns the `IDataService` for the grain which is calling the `DataServiceClient`. To do that, we use the `GetGrainService(GrainId)` overload in conjunction with the `CurrentGrainReference` property. +1. Create the grain service client. Clients typically act as proxies for the grain services they target, so you usually add a method for each method on the target service. These methods need to get a reference to the target grain service so they can call into it. The `GrainServiceClient` base class provides several overloads of the `GetGrainService` method that can return a grain reference corresponding to a `GrainId`, a numeric hash (`uint`), or a `SiloAddress`. The latter two overloads are for advanced cases where you want to use a different mechanism to map responsibility to hosts or address a host directly. In the sample code below, we define a property, `GrainService`, which returns the `IDataService` for the grain calling the `DataServiceClient`. To do that, we use the `GetGrainService(GrainId)` overload in conjunction with the `CurrentGrainReference` property. ```csharp public class DataServiceClient : GrainServiceClient, IDataServiceClient @@ -133,28 +131,7 @@ A is a special grain; one that has no stable } ``` -:::zone-end - -:::zone target="docs" pivot="orleans-3-x" - - -1. Create the actual grain service client. It pretty much just acts as a proxy for the data service. Unfortunately, you have to manually type in all the method mappings, which are just simple one-liners. - - ```csharp - public class DataServiceClient : GrainServiceClient, IDataServiceClient - { - public DataServiceClient(IServiceProvider serviceProvider) - : base(serviceProvider) - { - } - - public Task MyMethod() => GrainService.MyMethod(); - } - ``` - -:::zone-end - -1. Inject the grain service client into the other grains that need it. The `GrainServiceClient` is not guaranteed to access the `GrainService` on the local silo. Your command could potentially be sent to the `GrainService` on any silo in the cluster. +1. Inject the grain service client into the other grains that need it. The `GrainServiceClient` isn't guaranteed to access the `GrainService` on the local silo. Your command could potentially be sent to the `GrainService` on any silo in the cluster. ```csharp public class MyNormalGrain: Grain, INormalGrain @@ -168,7 +145,7 @@ A is a special grain; one that has no stable } ``` -1. Configure the grain service and grain service client in the silo. You need to do this so that the silo will start the `GrainService`. +1. Configure the grain service and grain service client in the silo. You need to do this so the silo starts the `GrainService`. ```csharp (ISiloHostBuilder builder) => @@ -180,7 +157,7 @@ A is a special grain; one that has no stable ## Additional notes -There's an extension method on which is used to register grain services. +There's an extension method, , used to register grain services. ```csharp services.AddSingleton( @@ -188,7 +165,7 @@ services.AddSingleton( ``` -The silo fetches `IGrainService` types from the service provider when starting: _orleans/src/Orleans.Runtime/Silo/Silo.cs_ +The silo fetches `IGrainService` types from the service provider when starting (see _orleans/src/Orleans.Runtime/Silo/Silo.cs_): ```csharp var grainServices = this.Services.GetServices(); @@ -205,7 +182,7 @@ The [Microsoft.Orleans.Runtime](https://www.nuget.org/packages/Microsoft.Orleans The [Microsoft.Orleans.OrleansRuntime](https://www.nuget.org/packages/Microsoft.Orleans.OrleansRuntime) NuGet package should be referenced by the `GrainService` project. :::zone-end -In order for this to work you have to register both the service and its client. The code looks something like this: +For this to work, you must register both the service and its client. The code looks something like this: ```csharp var builder = new HostBuilder() diff --git a/docs/orleans/grains/index.md b/docs/orleans/grains/index.md index 90111ac2105cb..68d21f1f5351b 100644 --- a/docs/orleans/grains/index.md +++ b/docs/orleans/grains/index.md @@ -1,18 +1,19 @@ --- title: Develop a grain description: Learn how to develop a grain in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Develop a grain -Before you write code to implement a grain class, create a new Class Library project targeting .NET Standard or .NET Core (preferred) or .NET Framework 4.6.1 or higher (if you cannot use .NET Standard or .NET Core due to dependencies). Grain interfaces and grain classes can be defined in the same Class Library project, or in two different projects for better separation of interfaces from implementation. In either case, the projects need to reference the [Microsoft.Orleans.Sdk](https://www.nuget.org/packages/Microsoft.Orleans.Sdk) NuGet package. +Before writing code to implement a grain class, create a new Class Library project targeting .NET Standard or .NET Core (preferred), or .NET Framework 4.6.1 or higher (if you cannot use .NET Standard or .NET Core due to dependencies). You can define grain interfaces and grain classes in the same Class Library project or in two different projects for better separation of interfaces from implementation. In either case, the projects need to reference the [Microsoft.Orleans.Sdk](https://www.nuget.org/packages/Microsoft.Orleans.Sdk) NuGet package. For more thorough instructions, see the [Project Setup](../tutorials-and-samples/tutorial-1.md#project-setup) section of [Tutorial One – Orleans Basics](../tutorials-and-samples/tutorial-1.md). ## Grain interfaces and classes -Grains interact with each other and get called from outside by invoking methods declared as part of the respective grain interfaces. A grain class implements one or more previously declared grain interfaces. All methods of a grain interface must return a (for `void` methods), a or a (for methods returning values of type `T`). +Grains interact with each other and are called from outside by invoking methods declared as part of their respective grain interfaces. A grain class implements one or more previously declared grain interfaces. All methods of a grain interface must return a (for `void` methods), a , or a (for methods returning values of type `T`). The following is an excerpt from the Orleans Presence Service sample: @@ -62,7 +63,7 @@ public class PlayerGrain : Grain, IPlayerGrain ## Response timeout for grain methods -The Orleans runtime allows you to enforce a response timeout per grain method. If a grain method doesn't complete within the timeout, the runtime throws the . To impose a response timeout, add the `ResponseTimeoutAttribute` to the interface's grain method definition. It's very important that the attribute is added to the interface method definition, not to the method implementation in the grain class, as both the client and the silo need to be aware of the timeout. +The Orleans runtime allows you to enforce a response timeout per grain method. If a grain method doesn't complete within the timeout, the runtime throws a . To impose a response timeout, add the `ResponseTimeoutAttribute` to the interface's grain method definition. It's crucial to add the attribute to the interface method definition, not the method implementation in the grain class, as both the client and the silo need to be aware of the timeout. Extending the previous `PlayerGrain` implementation, the following example shows how to impose a response timeout on the `LeaveGame` method: @@ -82,7 +83,7 @@ The preceding code sets a response timeout of five seconds on the `LeaveGame` me ### Configure response timeout -Much like individual grain method response timeouts, you can configure a default response timeout for all grain methods. Calls to grain methods will timeout if a response isn't received within a specified time period. By default, this period is **30 seconds**. You can configure the default response timeout: +Similar to individual grain method response timeouts, you can configure a default response timeout for all grain methods. Calls to grain methods time out if a response isn't received within the specified period. By default, this period is **30 seconds**. You can configure the default response timeout: - By configuring on , on an external client. - By configuring on , on a server. @@ -91,8 +92,8 @@ For more information on configuring Orleans, see [Client configuration](../host/ ## Return values from grain methods -A grain method that returns a value of type `T` is defined in a grain interface as returning a `Task`. -For grain methods not marked with the `async` keyword, when the return value is available, it is usually returned via the following statement: +Define a grain method that returns a value of type `T` in a grain interface as returning a `Task`. +For grain methods not marked with the `async` keyword, when the return value is available, you usually return it using the following statement: ```csharp public Task GrainMethod1() @@ -101,7 +102,7 @@ public Task GrainMethod1() } ``` -A grain method that returns no value, effectively a void method, is defined in a grain interface as returning a `Task`. The returned `Task` indicates asynchronous execution and completion of the method. For grain methods not marked with the `async` keyword, when a "void" method completes its execution, it needs to return the special value of : +Define a grain method that returns no value (effectively a void method) in a grain interface as returning a `Task`. The returned `Task` indicates the asynchronous execution and completion of the method. For grain methods not marked with the `async` keyword, when a "void" method completes its execution, it needs to return the special value : ```csharp public Task GrainMethod2() @@ -128,7 +129,7 @@ public async Task GrainMethod4() } ``` -If a grain method receives the return value from another asynchronous method call, to a grain or not, and doesn't need to perform error handling of that call, it can simply return the `Task` it receives from that asynchronous call: +If a grain method receives the return value from another asynchronous method call (to a grain or not) and doesn't need to perform error handling for that call, it can simply return the `Task` it receives from that asynchronous call: ```csharp public Task GrainMethod5() @@ -153,13 +154,13 @@ public Task GrainMethod6() ## Grain references -A Grain reference is a proxy object that implements the same grain interface as the corresponding grain class. It encapsulates the logical identity (type and unique key) of the target grain. Grain references are used for making calls to the target grain. Each grain reference is to a single grain (a single instance of the grain class), but one can create multiple independent references to the same grain. +A grain reference is a proxy object implementing the same grain interface as the corresponding grain class. It encapsulates the logical identity (type and unique key) of the target grain. You use grain references to make calls to the target grain. Each grain reference points to a single grain (a single instance of the grain class), but you can create multiple independent references to the same grain. -Since a grain reference represents the logical identity of the target grain, it is independent of the physical location of the grain, and stays valid even after a complete restart of the system. Developers can use grain references like any other .NET object. It can be passed to a method, used as a method return value, etc., and even saved to persistent storage. +Since a grain reference represents the logical identity of the target grain, it's independent of the grain's physical location and remains valid even after a complete system restart. You can use grain references like any other .NET object. You can pass it to a method, use it as a method return value, etc., and even save it to persistent storage. -A grain reference can be obtained by passing the identity of a grain to the method, where `T` is the grain interface and `key` is the unique key of the grain within the type. +You can obtain a grain reference by passing the identity of a grain to the method, where `T` is the grain interface and `key` is the unique key of the grain within its type. -The following are examples of how to obtain a grain reference of the `IPlayerGrain` interface defined above. +The following examples show how to obtain a grain reference for the `IPlayerGrain` interface defined previously. From inside a grain class: @@ -177,7 +178,7 @@ For more information about grain references, see the [grain reference article](g ### Grain method invocation -The Orleans programming model is based on [asynchronous programming](../../csharp/asynchronous-programming/index.md). Using the grain reference from the previous example, here's how to perform a grain method invocation: +The Orleans programming model is based on [asynchronous programming](../../csharp/asynchronous-programming/index.md). Using the grain reference from the previous example, here's how you perform a grain method invocation: ```csharp // Invoking a grain method asynchronously @@ -192,7 +193,7 @@ await joinGameTask; players.Add(playerId); ``` -It is possible to join two or more `Tasks`; the join operation creates a new `Task` that is resolved when all of its constituent `Task`s are completed. This is a useful pattern when a grain needs to start multiple computations and wait for all of them to complete before proceeding. For example, a front-end grain that generates a web page made of many parts might make multiple back-end calls, one for each part, and receive a `Task` for each result. The grain would then await the join of all of these `Tasks`; when the join `Task` is resolved, the individual `Task`s have been completed, and all the data required to format the web page has been received. +You can join two or more `Tasks`. The join operation creates a new `Task` that resolves when all its constituent `Tasks` complete. This pattern is useful when a grain needs to start multiple computations and wait for all of them to complete before proceeding. For example, a front-end grain generating a web page made of many parts might make multiple back-end calls (one for each part) and receive a `Task` for each result. The grain would then await the join of all these `Tasks`. When the joined `Task` resolves, the individual `Tasks` have completed, and all the data required to format the web page has been received. Example: @@ -217,15 +218,15 @@ await joinedTask; ### Error propagation -When a grain method throws an exception, Orleans propagates that exception up the calling stack, across hosts as necessary. For this to work as intended, exceptions must be serializable by Orleans and hosts which are handling the exception must have the exception type available. If an exception type isn't available, the exception will be thrown as an instance of , preserving the message, type, and stack trace of the original exception. +When a grain method throws an exception, Orleans propagates that exception up the calling stack, across hosts as necessary. For this to work as intended, exceptions must be serializable by Orleans, and hosts handling the exception must have the exception type available. If an exception type isn't available, Orleans throws the exception as an instance of , preserving the message, type, and stack trace of the original exception. -Exceptions thrown from grain methods don't cause the grain to be deactivated unless the exception inherits from . `InconsistentStateException` is thrown by storage operations which discover that the grain's in-memory state is inconsistent with the state in the database. Aside from the special-casing of `InconsistentStateException`, this behavior is similar to throwing an exception from any .NET object: exceptions don't cause an object to be destroyed. +Exceptions thrown from grain methods don't cause the grain to be deactivated unless the exception inherits from . Storage operations throw `InconsistentStateException` when they discover that the grain's in-memory state is inconsistent with the state in the database. Aside from the special handling of `InconsistentStateException`, this behavior is similar to throwing an exception from any .NET object: exceptions don't cause an object to be destroyed. ### Virtual methods -A grain class can optionally override the and virtual methods; these methods are invoked by the Orleans runtime upon activation and deactivation of each grain of the class. This gives the grain code a chance to perform additional initialization and cleanup operations. An exception thrown by `OnActivateAsync` fails the activation process. +A grain class can optionally override the and virtual methods. The Orleans runtime invokes these methods upon activation and deactivation of each grain of the class. This gives your grain code a chance to perform additional initialization and cleanup operations. An exception thrown by `OnActivateAsync` fails the activation process. -While `OnActivateAsync`, if overridden, is always called as part of the grain activation process, `OnDeactivateAsync` is not guaranteed to get called in all situations, for example, in case of a server failure or other abnormal event. Because of that, applications should not rely on `OnDeactivateAsync` for performing critical operations such as the persistence of state changes. They should use it only for best-effort operations. +While `OnActivateAsync` (if overridden) is always called as part of the grain activation process, `OnDeactivateAsync` isn't guaranteed to be called in all situations (for example, in case of a server failure or other abnormal events). Because of this, your applications shouldn't rely on `OnDeactivateAsync` for performing critical operations, such as persisting state changes. Use it only for best-effort operations. ## See also diff --git a/docs/orleans/grains/interceptors.md b/docs/orleans/grains/interceptors.md index 7f05f4f4a72e1..2e6a731367e0e 100644 --- a/docs/orleans/grains/interceptors.md +++ b/docs/orleans/grains/interceptors.md @@ -1,23 +1,24 @@ --- title: Grain call filters -description: Learn about grain call filter in .NET Orleans. -ms.date: 07/03/2024 +description: Learn about grain call filters in .NET Orleans. +ms.date: 05/23/2025 +ms.topic: conceptual --- # Grain call filters -Grain call filters provide a means for intercepting grain calls. Filters can execute code both before and after a grain call. Multiple filters can be installed simultaneously. Filters are asynchronous and can modify , arguments, and the return value of the method being invoked. Filters can also inspect the of the method being invoked on the grain class and can be used to throw or handle exceptions. +Grain call filters provide a way to intercept grain calls. Filters can execute code both before and after a grain call. You can install multiple filters simultaneously. Filters are asynchronous and can modify , arguments, and the return value of the method being invoked. Filters can also inspect the of the method being invoked on the grain class and can be used to throw or handle exceptions. -Some example usages of grain call filters are: +Some example uses of grain call filters are: -* Authorization: a filter can inspect the method being invoked and the arguments or some authorization information in the `RequestContext` to determine whether or not to allow the call to proceed. -* Logging/Telemetry: a filter can log information and capture timing data and other statistics about method invocation. -* Error Handling: a filter can intercept exceptions thrown by a method invocation and transform it into another exception or handle the exception as it passes through the filter. +- **Authorization**: A filter can inspect the method being invoked and the arguments, or authorization information in the `RequestContext`, to determine whether to allow the call to proceed. +- **Logging/Telemetry**: A filter can log information and capture timing data and other statistics about method invocation. +- **Error Handling**: A filter can intercept exceptions thrown by a method invocation and transform them into other exceptions or handle the exceptions as they pass through the filter. -Filters come in two flavors: +Filters come in two types: -* Incoming call filters -* Outgoing call filters +- Incoming call filters +- Outgoing call filters Incoming call filters are executed when receiving a call. Outgoing call filters are executed when making a call. @@ -69,15 +70,15 @@ public interface IIncomingGrainCallContext } ``` -The `IIncomingGrainCallFilter.Invoke(IIncomingGrainCallContext)` method must await or return the result of `IIncomingGrainCallContext.Invoke()` to execute the next configured filter and eventually the grain method itself. The `Result` property can be modified after awaiting the `Invoke()` method. The `ImplementationMethod` property returns the `MethodInfo` of the implementation class. The `MethodInfo` of the interface method can be accessed using the `InterfaceMethod` property. Grain call filters are called for all method calls to a grain and this includes calls to grain extensions (implementations of `IGrainExtension`) which are installed in the grain. For example, grain extensions are used to implement Streams and Cancellation Tokens. Therefore, it should be expected that the value of `ImplementationMethod` is not always a method in the grain class itself. +The `IIncomingGrainCallFilter.Invoke(IIncomingGrainCallContext)` method must `await` or return the result of `IIncomingGrainCallContext.Invoke()` to execute the next configured filter and eventually the grain method itself. You can modify the `Result` property after awaiting the `Invoke()` method. The `ImplementationMethod` property returns the `MethodInfo` of the implementation class. You can access the `MethodInfo` of the interface method using the `InterfaceMethod` property. Grain call filters are called for all method calls to a grain, including calls to grain extensions (implementations of `IGrainExtension`) installed in the grain. For example, Orleans uses grain extensions to implement Streams and Cancellation Tokens. Therefore, expect that the value of `ImplementationMethod` isn't always a method in the grain class itself. ## Configure incoming grain call filters -Implementations of can either be registered as silo-wide filters via Dependency Injection or they can be registered as grain-level filters via a grain implementing `IIncomingGrainCallFilter` directly. +You can register implementations of either as silo-wide filters via Dependency Injection or as grain-level filters by having a grain implement `IIncomingGrainCallFilter` directly. ### Silo-wide grain call filters -A delegate can be registered as a silo-wide grain call filter using Dependency Injection like so: +You can register a delegate as a silo-wide grain call filter using Dependency Injection like this: ```csharp siloHostBuilder.AddIncomingGrainCallFilter(async context => @@ -102,7 +103,7 @@ siloHostBuilder.AddIncomingGrainCallFilter(async context => }); ``` -Similarly, a class can be registered as a grain call filter using the helper method. Here is an example of a grain call filter that logs the results of every grain method: +Similarly, you can register a class as a grain call filter using the helper method. Here's an example of a grain call filter that logs the results of every grain method: ```csharp public class LoggingCallFilter : IIncomingGrainCallFilter @@ -158,9 +159,9 @@ siloHostBuilder.ConfigureServices( services => services.AddSingleton()); ``` -### Per-grain Grain call filters +### Per-grain grain call filters -A grain class can register itself as a grain call filter and filter any calls made to it by implementing `IIncomingGrainCallFilter` like so: +A grain class can register itself as a grain call filter and filter any calls made to it by implementing `IIncomingGrainCallFilter` like this: ```csharp public class MyFilteredGrain @@ -183,9 +184,9 @@ public class MyFilteredGrain } ``` -In the above example, all calls to the `GetFavoriteNumber` method will return `38` instead of `7`, because the return value has been altered by the filter. +In the preceding example, all calls to the `GetFavoriteNumber` method return `38` instead of `7` because the filter altered the return value. -Another use case for filters is in access control, as in this example: +Another use case for filters is access control, as shown in this example: ```csharp [AttributeUsage(AttributeTargets.Method)] @@ -213,21 +214,21 @@ public class MyAccessControlledGrain } ``` -In the above example, the `SpecialAdminOnlyOperation` method can only be called if `"isAdmin"` is set to `true` in the `RequestContext`. In this way, grain call filters can be used for authorization. In this example, it is the responsibility of the caller to ensure that the `"isAdmin"` value is set correctly and that authentication is performed correctly. Note that the `[AdminOnly]` attribute is specified on the grain class method. This is because the `ImplementationMethod` property returns the `MethodInfo` of the implementation, not the interface. The filter could also check the `InterfaceMethod` property. +In the preceding example, the `SpecialAdminOnlyOperation` method can only be called if `"isAdmin"` is set to `true` in the `RequestContext`. In this way, you can use grain call filters for authorization. In this example, it's the caller's responsibility to ensure the `"isAdmin"` value is set correctly and that authentication is performed correctly. Note that the `[AdminOnly]` attribute is specified on the grain class method. This is because the `ImplementationMethod` property returns the `MethodInfo` of the implementation, not the interface. The filter could also check the `InterfaceMethod` property. ## Grain call filter ordering -Grain call filters follow a defined ordering: +Grain call filters follow a defined order: 1. `IIncomingGrainCallFilter` implementations configured in the dependency injection container, in the order in which they are registered. 1. Grain-level filter, if the grain implements `IIncomingGrainCallFilter`. 1. Grain method implementation or grain extension method implementation. -Each call to `IIncomingGrainCallContext.Invoke()` encapsulates the next defined filter so that each filter has a chance to execute code before and after the next filter in the chain and eventually the grain method itself. +Each call to `IIncomingGrainCallContext.Invoke()` encapsulates the next defined filter, allowing each filter a chance to execute code before and after the next filter in the chain and, eventually, the grain method itself. ## Outgoing call filters -Outgoing grain call filters are similar to incoming grain call filters with the major difference being that they are invoked on the caller (client) rather than the callee (grain). +Outgoing grain call filters are similar to incoming grain call filters. The major difference is that they are invoked on the caller (client) rather than the callee (grain). Outgoing grain call filters implement the `IOutgoingGrainCallFilter` interface, which has one method: @@ -270,13 +271,13 @@ public interface IOutgoingGrainCallContext } ``` -The `IOutgoingGrainCallFilter.Invoke(IOutgoingGrainCallContext)` method must await or return the result of `IOutgoingGrainCallContext.Invoke()` to execute the next configured filter and eventually the grain method itself. The `Result` property can be modified after awaiting the `Invoke()` method. The `MethodInfo` of the interface method being called can be accessed using the `InterfaceMethod` property. Outgoing grain call filters are invoked for all method calls to a grain and this includes calls to system methods made by Orleans. +The `IOutgoingGrainCallFilter.Invoke(IOutgoingGrainCallContext)` method must `await` or return the result of `IOutgoingGrainCallContext.Invoke()` to execute the next configured filter and eventually the grain method itself. You can modify the `Result` property after awaiting the `Invoke()` method. You can access the `MethodInfo` of the interface method being called using the `InterfaceMethod` property. Outgoing grain call filters are invoked for all method calls to a grain, including calls to system methods made by Orleans. ## Configure outgoing grain call filters -Implementations of `IOutgoingGrainCallFilter` can either be registered on both silos and clients using Dependency Injection. +You can register implementations of `IOutgoingGrainCallFilter` on both silos and clients using Dependency Injection. -A delegate can be registered as a call filter like so: +Register a delegate as a call filter like this: ```csharp builder.AddOutgoingGrainCallFilter(async context => @@ -303,8 +304,7 @@ builder.AddOutgoingGrainCallFilter(async context => In the above code, `builder` may be either an instance of or . -Similarly, a class can be registered as an outgoing grain call filter. -Here is an example of a grain call filter that logs the results of every grain method: +Similarly, you can register a class as an outgoing grain call filter. Here's an example of a grain call filter that logs the results of every grain method: ```csharp public class LoggingCallFilter : IOutgoingGrainCallFilter @@ -366,19 +366,19 @@ As with the delegate call filter example, `builder` may be an instance of either ### Exception conversion -When an exception that has been thrown from the server is getting deserialized on the client, you may sometimes get the following exception instead of the actual one: `TypeLoadException: Could not find Whatever.dll.` +When an exception thrown from the server is deserialized on the client, you might sometimes get the following exception instead of the actual one: `TypeLoadException: Could not find Whatever.dll.` -This happens if the assembly containing the exception is not available to the client. For example, say you are using Entity Framework in your grain implementations; then an `EntityException` may be thrown. The client on the other hand does not (and should not) reference `EntityFramework.dll` since it does not know the underlying data access layer. +This happens if the assembly containing the exception isn't available to the client. For example, say you use Entity Framework in your grain implementations; an `EntityException` might be thrown. The client, on the other hand, doesn't (and shouldn't) reference `EntityFramework.dll` since it doesn't know the underlying data access layer. -When the client tries to deserialize the `EntityException`, it will fail due to the missing DLL; as a consequence, a is thrown hiding the original `EntityException`. +When the client tries to deserialize the `EntityException`, it fails due to the missing DLL. As a consequence, a is thrown, hiding the original `EntityException`. -One may argue that this is pretty okay since the client would never handle the `EntityException`; otherwise, it would have to reference `EntityFramework.dll`. +One might argue this is acceptable since the client would never handle the `EntityException`; otherwise, it would need to reference `EntityFramework.dll`. But what if the client wants at least to log the exception? The problem is that the original error message is lost. One way to work around this issue is to intercept server-side exceptions and replace them with plain exceptions of type `Exception` if the exception type is presumably unknown on the client-side. -However, there is one important thing we have to keep in mind: we only want to replace an exception **if the caller is the grain client**. We don't want to replace an exception if the caller is another grain (or the Orleans infrastructure which is making grain calls, too; for example, on the `GrainBasedReminderTable` grain). +However, keep one important thing in mind: you only want to replace an exception **if the caller is the grain client**. You don't want to replace an exception if the caller is another grain (or the Orleans infrastructure making grain calls, for example,, on the `GrainBasedReminderTable` grain). -On the server-side this can be done with a silo-level interceptor: +On the server-side, you can do this with a silo-level interceptor: ```csharp public class ExceptionConversionFilter : IIncomingGrainCallFilter @@ -462,11 +462,11 @@ clientBuilder.AddOutgoingGrainCallFilter(context => }); ``` -This way the client tells the server that it wants to use exception conversion. +This way, the client tells the server it wants to use exception conversion. ### Call grains from interceptors -It is possible to make grain calls from an interceptor by injecting into the interceptor class: +You can make grain calls from an interceptor by injecting into the interceptor class: ```csharp private readonly IGrainFactory _grainFactory; diff --git a/docs/orleans/grains/observers.md b/docs/orleans/grains/observers.md index 6458f5504eb8b..45292a3f7865a 100644 --- a/docs/orleans/grains/observers.md +++ b/docs/orleans/grains/observers.md @@ -1,28 +1,27 @@ --- title: Observers description: Learn about observers in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Observers -There are situations in which a simple message/response pattern is not enough, and the client needs to receive asynchronous notifications. -For example, a user might want to be notified when a new instant message has been published by a friend. +Sometimes, a simple message/response pattern isn't enough, and the client needs to receive asynchronous notifications. For example, a user might want notification when a friend publishes a new instant message. -Client observers is a mechanism that allows notifying clients asynchronously. Observer interfaces must inherit from , and all methods must return either `void`, , , , or . A return type of `void` is not recommended as it may encourage the use of `async void` on the implementation, which is a dangerous pattern since it can result in application crashes if an exception is thrown from the method. Instead, for best-effort notification scenarios, consider applying the to the observer's interface method. This will cause the receiver to not send a response for the method invocation and will cause the method to return immediately at the call site, without waiting for a response from the observer. A grain calls a method on an observer by invoking it like any grain interface method. The Orleans runtime will ensure the delivery of requests and responses. A common use case for observers is to enlist a client to receive notifications when an event occurs in the Orleans application. A grain that publishes such notifications should provide an API to add or remove observers. In addition, it is usually convenient to expose a method that allows an existing subscription to be cancelled. +Client observers are a mechanism allowing asynchronous notification of clients. Observer interfaces must inherit from , and all methods must return either `void`, , , , or . We don't recommend a return type of `void` because it might encourage using `async void` in the implementation. This is a dangerous pattern, as it can cause application crashes if an exception is thrown from the method. Instead, for best-effort notification scenarios, consider applying the to the observer's interface method. This causes the receiver not to send a response for the method invocation and makes the method return immediately at the call site, without waiting for a response from the observer. A grain calls a method on an observer by invoking it like any grain interface method. The Orleans runtime ensures the delivery of requests and responses. A common use case for observers is enlisting a client to receive notifications when an event occurs in the Orleans application. A grain publishing such notifications should provide an API to add or remove observers. Additionally, it's usually convenient to expose a method allowing cancellation of an existing subscription. -Grain developers may use a utility class such as to simplify development of observed grain types. Unlike grains, which are automatically reactivated as-needed after failure, clients are not fault-tolerant: a client which fails may never recover. -For this reason, the `ObserverManager` utility removes subscriptions after a configured duration. Clients which are active should resubscribe on a timer to keep their subscription active. +You can use a utility class like to simplify the development of observed grain types. Unlike grains, which Orleans automatically reactivates as needed after failure, clients aren't fault-tolerant: a client that fails might never recover. For this reason, the `ObserverManager` utility removes subscriptions after a configured duration. Active clients should resubscribe on a timer to keep their subscriptions active. -To subscribe to a notification, the client must first create a local object that implements the observer interface. It then calls a method on the observer factory, `, to turn the object into a grain reference, which can then be passed to the subscription method on the notifying grain. +To subscribe to a notification, the client must first create a local object implementing the observer interface. It then calls a method on the observer factory, `, to turn the object into a grain reference. You can then pass this reference to the subscription method on the notifying grain. -This model can also be used by other grains to receive asynchronous notifications. Grains can also implement interfaces. Unlike in the client subscription case, the subscribing grain simply implements the observer interface and passes in a reference to itself (for example, `this.AsReference()`). There is no need for `CreateObjectReference()` because grains are already addressable. +Other grains can also use this model to receive asynchronous notifications. Grains can implement interfaces. Unlike the client subscription case, the subscribing grain simply implements the observer interface and passes in a reference to itself (for example,, `this.AsReference()`). There's no need for `CreateObjectReference()` because grains are already addressable. ## Code example -Let's assume that we have a grain that periodically sends messages to clients. For simplicity, the message in our example will be a string. We first define the interface on the client that will receive the message. +Let's assume you have a grain that periodically sends messages to clients. For simplicity, the message in our example is a string. First, define the interface on the client that receives the message. -The interface will look like this +The interface looks like this: ```csharp public interface IChat : IGrainObserver @@ -31,10 +30,10 @@ public interface IChat : IGrainObserver } ``` -The only special thing is that the interface should inherit from `IGrainObserver`. -Now any client that wants to observe those messages should implement a class that implements `IChat`. +The only special requirement is that the interface must inherit from `IGrainObserver`. +Now, any client wanting to observe these messages should implement a class that implements `IChat`. -The simplest case would be something like this: +The simplest case looks something like this: ```csharp public class Chat : IChat @@ -47,7 +46,7 @@ public class Chat : IChat } ``` -On the server, we should next have a Grain that sends these chat messages to clients. The Grain should also have a mechanism for clients to subscribe and unsubscribe themselves for notifications. For subscriptions, the grain can use an instance of the utility class . +On the server, you should next have a grain that sends these chat messages to clients. The grain should also provide a mechanism for clients to subscribe and unsubscribe from notifications. For subscriptions, the grain can use an instance of the utility class . > [!NOTE] > is part of Orleans since version 7.0. For older versions, the following [implementation](https://github.com/dotnet/orleans/blob/e997335d2d689bb39e67f6bcf6fd70862a22c02f/test/Grains/TestGrains/ObserverManager.cs#L12) can be copied. @@ -82,7 +81,7 @@ class HelloGrain : Grain, IHello } ``` -To send a message to clients, the `Notify` method of the `ObserverManager` instance can be used. The method takes an `Action` method or lambda expression (where `T` is of type `IChat` here). You can call any method on the interface to send it to clients. In our case we only have one method, `ReceiveMessage`, and our sending code on the server would look like this: +To send a message to clients, use the `Notify` method of the `ObserverManager` instance. The method takes an `Action` method or lambda expression (where `T` is of type `IChat` here). You can call any method on the interface to send it to clients. In our case, we only have one method, `ReceiveMessage`, and our sending code on the server looks like this: ```csharp public Task SendUpdateMessage(string message) @@ -93,9 +92,9 @@ public Task SendUpdateMessage(string message) } ``` -Now our server has a method to send messages to observer clients, two methods for subscribing/unsubscribing, and the client has implemented a class able to observe the grain messages. The last step is to create an observer reference on the client using our previously implemented `Chat` class, and let it receive the messages after subscribing to it. +Now, our server has a method to send messages to observer clients and two methods for subscribing/unsubscribing. The client has implemented a class capable of observing the grain messages. The final step is to create an observer reference on the client using our previously implemented `Chat` class and let it receive messages after subscribing. -The code would look like this: +The code looks like this: ```csharp //First create the grain reference @@ -109,16 +108,16 @@ var obj = _grainFactory.CreateObjectReference(c); await friend.Subscribe(obj); ``` -Now whenever our grain on the server calls the `SendUpdateMessage` method, all subscribed clients will receive the message. In our client code, the `Chat` instance in variable `c` will receive the message and output it to the console. +Now, whenever our grain on the server calls the `SendUpdateMessage` method, all subscribed clients receive the message. In our client code, the `Chat` instance in the variable `c` receives the message and outputs it to the console. > [!IMPORTANT] -> Objects passed to `CreateObjectReference` are held via a and will therefore be garbage collected if no other references exist. +> Objects passed to `CreateObjectReference` are held via a and are therefore garbage collected if no other references exist. -Users should maintain a reference for each observer which they do not want to be collected. +You should maintain a reference for each observer you don't want collected. > [!NOTE] -> Observers are inherently unreliable since a client which hosts an observer may fail and observers created after recovery have different (randomized) identities. relies on periodic resubscription by observers, as discussed above, so that inactive observers can be removed. +> Observers are inherently unreliable because a client hosting an observer might fail, and observers created after recovery have different (randomized) identities. relies on periodic resubscription by observers, as discussed above, so it can remove inactive observers. ## Execution model -Implementations of are registered via a call to and each call to that method creates a new reference that points to that implementation. Orleans will execute requests sent to each one of these references one-by-one, to completion. Observers are non-reentrant and therefore concurrent requests to an observer will not be interleaved by Orleans. If there are multiple observers which are receiving requests concurrently, those requests can execute in parallel. Execution of observer methods are not affected by attributes such as or : the execution model cannot be customized by a developer. +Implementations of are registered via a call to . Each call to that method creates a new reference pointing to that implementation. Orleans executes requests sent to each of these references one by one, to completion. Observers are non-reentrant; therefore, Orleans doesn't interleave concurrent requests to an observer. If multiple observers receive requests concurrently, those requests can execute in parallel. Attributes such as or don't affect the execution of observer methods; you cannot customize the execution model. diff --git a/docs/orleans/grains/oneway.md b/docs/orleans/grains/oneway.md index 74a65a6e5810f..bd71fc586827d 100644 --- a/docs/orleans/grains/oneway.md +++ b/docs/orleans/grains/oneway.md @@ -1,16 +1,17 @@ --- title: One-way requests description: Learn about one-way requests in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # One-way requests -Grains perform asynchronous request execution, requiring all grain interface methods to return an asynchronous type, like . Awaiting the completion of a task returned from a grain call notifies the caller that the request has finished, allowing them to handle any exceptions or receive return values. Orleans also supports *one-way requests*, enabling callers to notify a grain about an event without expecting exceptions or completion signals. +Grains perform asynchronous request execution, requiring all grain interface methods to return an asynchronous type, like . Awaiting the completion of a task returned from a grain call notifies the caller that the request has finished, allowing the caller to handle any exceptions or receive return values. Orleans also supports *one-way requests*, enabling callers to notify a grain about an event without expecting exceptions or completion signals. -One-way requests return to the caller immediately and don't signal failure or completion. A one-way request doesn't even guarantee that the callee received the request. The primary benefit of a one-way request is that they save messaging costs associated with sending a response back to the caller and can therefore improve performance in some specialized cases. One-way requests are an advanced performance feature and should be used with care and only when a developer has determined that a one-way request is beneficial. It's recommended to prefer regular bi-directional requests, which signal completion and propagate errors back to callers. +One-way requests return to the caller immediately and don't signal failure or completion. A one-way request doesn't even guarantee the callee received the request. The primary benefit of one-way requests is that they save messaging costs associated with sending a response back to the caller and can therefore improve performance in some specialized cases. One-way requests are an advanced performance feature. Use them with care and only when you determine that a one-way request is beneficial. We recommend preferring regular bidirectional requests, which signal completion and propagate errors back to callers. -A request can be made one way by marking the grain interface method with the , like so: +You can make a request one-way by marking the grain interface method with the , like this: ```csharp public interface IOneWayGrain : IGrainWithGuidKey @@ -20,4 +21,4 @@ public interface IOneWayGrain : IGrainWithGuidKey } ``` -One-way requests must return either or and must not return generic variants of those types ( and ). +One-way requests must return either or . They must not return generic variants of those types ( and ). diff --git a/docs/orleans/grains/request-context.md b/docs/orleans/grains/request-context.md index 0bff136fe7591..63d1e7189137e 100644 --- a/docs/orleans/grains/request-context.md +++ b/docs/orleans/grains/request-context.md @@ -1,39 +1,40 @@ --- title: Request context description: Learn about request context in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Request context -The is an Orleans feature that allows application metadata, such as a trace ID, to flow with requests. Application metadata may be added on the client; it will flow with Orleans requests to the receiving grain. The feature is implemented by a public static class, `RequestContext`, in the Orleans namespace. This class exposes two simple methods: +The is an Orleans feature allowing application metadata, such as a trace ID, to flow with requests. You can add application metadata on the client; it flows with Orleans requests to the receiving grain. The feature is implemented by a public static class, `RequestContext`, in the Orleans namespace. This class exposes two simple methods: ```csharp void Set(string key, object value) ``` -The preceding API is used to store a value in the request context. The value can be any serializable type. +Use the preceding API to store a value in the request context. The value can be any serializable type. ```csharp object Get(string key) ``` -The preceding API is used to retrieve a value from the current request context. +Use the preceding API to retrieve a value from the current request context. -The backing storage for `RequestContext` is async-local. When a caller (whether client-side or within Orleans) sends a request, the contents of the caller's `RequestContext` are included with the Orleans message for the request; when the grain code receives the request, that metadata is accessible from the local `RequestContext`. If the grain code does not modify the `RequestContext`, then any grain it requests to will receive the same metadata, and so on. +The backing storage for `RequestContext` is async-local. When a caller (client-side or within Orleans) sends a request, the contents of the caller's `RequestContext` are included with the Orleans message for the request. When the grain code receives the request, that metadata is accessible from the local `RequestContext`. If the grain code doesn't modify the `RequestContext`, then any grain it requests receives the same metadata, and so on. -Application metadata also is maintained when you schedule a future computation using or ; in both cases, the continuation will execute with the same metadata as the scheduling code had at the moment the computation was scheduled (that is, the system makes a copy of the current metadata and passes it to the continuation, so changes after the call to `StartNew` or `ContinueWith` will not be seen by the continuation). +Application metadata is also maintained when you schedule a future computation using or . In both cases, the continuation executes with the same metadata as the scheduling code had when the computation was scheduled. That is, the system copies the current metadata and passes it to the continuation, so the continuation won't see changes made after the call to `StartNew` or `ContinueWith`. > [!IMPORTANT] -> The application metadata does not flow back with responses; that is, code that runs as a result of a response being received, either within a `ContinueWith` continuation or after a call to or `GetValue`, will still run within the current context that was set by the original request. +> Application metadata doesn't flow back with responses. Code that runs as a result of receiving a response (either within a `ContinueWith` continuation or after a call to or `GetValue`) still runs within the current context set by the original request. -For example, to set a trace id in the client to a new `Guid`, you call: +For example, to set a trace ID in the client to a new `Guid`, call: ```csharp RequestContext.Set("TraceId", Guid.NewGuid()); ``` -Within grain code (or other code that runs within Orleans on a scheduler thread), the trace id of the original client request could be used, for instance, when writing a log: +Within grain code (or other code running within Orleans on a scheduler thread), you could use the trace ID of the original client request, for instance, when writing a log: ```csharp Logger.LogInformation( @@ -41,11 +42,11 @@ Logger.LogInformation( RequestContext.Get("TraceId")); ``` -While any serializable `object` may be sent as application metadata, it's worth mentioning that large or complex objects may add noticeable overhead to message serialization time. For this reason, the use of simple types (strings, GUIDs, or numeric types) is recommended. +While you can send any serializable `object` as application metadata, it's worth mentioning that large or complex objects might add noticeable overhead to message serialization time. For this reason, we recommend using simple types (strings, GUIDs, or numeric types). ## Example grain code -To help illustrate the use of a request context, consider the following example grain code: +To help illustrate the use of request context, consider the following example grain code: ```csharp using GrainInterfaces; @@ -78,11 +79,11 @@ public interface IHelloGrain : IGrainWithStringKey } ``` -The `SayHello` method logs the incoming `greeting` parameter and then retrieves the trace id from the request context. If no trace id is found, the grain logs "No trace ID". +The `SayHello` method logs the incoming `greeting` parameter and then retrieves the trace ID from the request context. If no trace ID is found, the grain logs "No trace ID". ## Example client code -The client is able to set the trace id in the request context before calling the `SayHello` method on the `HelloGrain`. The following client code demonstrates how to set a trace id in the request context and call the `SayHello` method on the `HelloGrain`: +The client can set the trace ID in the request context before calling the `SayHello` method on the `HelloGrain`. The following client code demonstrates how to set a trace ID in the request context and call the `SayHello` method on the `HelloGrain`: ```csharp using GrainInterfaces; @@ -112,4 +113,4 @@ Console.WriteLine(message); // Client said: "Good morning!", so HelloGrain says: Hello! ``` -In this example, the client sets the trace id to "example-id-set-by-client" before calling the `SayHello` method on the `HelloGrain`. The grain retrieves the trace id from the request context and logs it. +In this example, the client sets the trace ID to "example-id-set-by-client" before calling the `SayHello` method on the `HelloGrain`. The grain retrieves the trace ID from the request context and logs it. diff --git a/docs/orleans/grains/request-scheduling.md b/docs/orleans/grains/request-scheduling.md index 98a93d8ed0a78..2346c88a69393 100644 --- a/docs/orleans/grains/request-scheduling.md +++ b/docs/orleans/grains/request-scheduling.md @@ -1,12 +1,13 @@ --- title: Request scheduling description: Learn about request scheduling in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Request scheduling -Grain activations have a *single-threaded* execution model and, by default, process each request from beginning to completion before the next request can begin processing. In some circumstances, it may be desirable for activation to process other requests while one request is waiting for an asynchronous operation to complete. For this and other reasons, Orleans gives the developer some control over the request interleaving behavior, as described in the [Reentrancy](#reentrancy) section. What follows is an example of non-reentrant request scheduling, which is the default behavior in Orleans. +Grain activations have a *single-threaded* execution model. By default, they process each request from beginning to completion before the next request can begin processing. In some circumstances, it might be desirable for an activation to process other requests while one request waits for an asynchronous operation to complete. For this and other reasons, Orleans gives you some control over the request interleaving behavior, as described in the [Reentrancy](#reentrancy) section. What follows is an example of non-reentrant request scheduling, which is the default behavior in Orleans. Consider the following `PingGrain` definition: @@ -50,7 +51,7 @@ The flow of execution is as follows: 1. *B* returns immediately from `Ping()` back to *A*. 1. *A* logs `"2"` and returns back to the original caller. -While *A* is awaiting the call to *B*, it can't process any incoming requests. As a result, if *A* and *B* were to call each other simultaneously, they may *deadlock* while waiting for those calls to complete. Here's an example, based on the client issuing the following call: +While *A* awaits the call to *B*, it can't process any incoming requests. As a result, if *A* and *B* were to call each other simultaneously, they might *deadlock* while waiting for those calls to complete. Here's an example, based on the client issuing the following call: ```csharp var a = grainFactory.GetGrain("A"); @@ -61,7 +62,7 @@ var b = grainFactory.GetGrain("B"); await Task.WhenAll(a.CallOther(b), b.CallOther(a)); ``` -## Case 1: the calls don't deadlock +## Case 1: The calls don't deadlock :::image type="content" source="grain-persistence/media/reentrancy-scheduling-diagram-01.png" alt-text="Reentrancy scheduling diagram without deadlock." lightbox="grain-persistence/media/reentrancy-scheduling-diagram-01.png"::: @@ -73,9 +74,9 @@ In this example: 1. When *B* issues its `Ping()` call to *A*, *A* is still busy logging its message (`"2"`), so the call has to wait for a short duration, but it's soon able to be processed. 1. *A* processes the `Ping()` call and returns to *B*, which returns to the original caller. -Consider a less fortunate series of events: one in which the same code results in a *deadlock* due to slightly different timing. +Consider a less fortunate series of events where the same code results in a *deadlock* due to slightly different timing. -## Case 2: the calls deadlock +## Case 2: The calls deadlock :::image type="content" source="grain-persistence/media/reentrancy-scheduling-diagram-02.png" alt-text="Reentrancy scheduling diagram with deadlock." lightbox="grain-persistence/media/reentrancy-scheduling-diagram-02.png"::: @@ -83,39 +84,39 @@ In this example: 1. The `CallOther` calls arrive at their respective grains and are processed simultaneously. 1. Both grains log `"1"` and proceed to `await other.Ping()`. -1. Since both grains are still *busy* (processing the `CallOther` request, which hasn't finished yet), the `Ping()` requests wait -1. After a while, Orleans determines that the call has **timed out** and each `Ping()` call results in an exception being thrown. -1. The `CallOther` method body doesn't handle the exception and it bubbles up to the original caller. +1. Since both grains are still *busy* (processing the `CallOther` request, which hasn't finished yet), the `Ping()` requests wait. +1. After a while, Orleans determines that the call has **timed out**, and each `Ping()` call results in an exception being thrown. +1. The `CallOther` method body doesn't handle the exception, and it bubbles up to the original caller. -The following section describes how to prevent deadlocks by allowing multiple requests to interleave their execution with each other. +The following section describes how to prevent deadlocks by allowing multiple requests to interleave their execution. ## Reentrancy -Orleans defaults to choosing a safe execution flow: one in which the internal state of a grain isn't modified concurrently during multiple requests. Concurrent modification of the internal state complicates logic and puts a greater burden on the developer. This protection against those kinds of concurrency bugs has a cost, which was previously discussed, primarily *liveness*: certain call patterns can lead to deadlocks. One way to avoid deadlocks is to ensure that grain calls never result in a cycle. Often, it's difficult to write code that is cycle-free and can't deadlock. Waiting for each request to run from beginning to completion before processing the next request can also hurt performance. For example, by default, if a grain method performs some asynchronous request to a database service then the grain pauses request execution until the response from the database arrives at the grain. +Orleans defaults to a safe execution flow where the internal state of a grain isn't modified concurrently by multiple requests. Concurrent modification complicates logic and places a greater burden on you, the developer. This protection against concurrency bugs has a cost, primarily *liveness*: certain call patterns can lead to deadlocks, as discussed previously. One way to avoid deadlocks is to ensure grain calls never form a cycle. Often, it's difficult to write code that is cycle-free and guaranteed not to deadlock. Waiting for each request to run from beginning to completion before processing the next can also hurt performance. For example, by default, if a grain method performs an asynchronous request to a database service, the grain pauses request execution until the database response arrives. -Each of those cases is discussed in the sections that follow. For these reasons, Orleans provides developers with options to allow some or all requests to be executed *concurrently*, interleaving their execution with each other. In Orleans, such concerns are referred to as *reentrancy* or *interleaving*. By executing requests concurrently, grains that perform asynchronous operations can process more requests in a shorter period. +Each of these cases is discussed in the following sections. For these reasons, Orleans provides options to allow some or all requests to execute *concurrently*, interleaving their execution. In Orleans, we refer to such concerns as *reentrancy* or *interleaving*. By executing requests concurrently, grains performing asynchronous operations can process more requests in a shorter period. Multiple requests may be interleaved in the following cases: -* The grain class is marked with . -* The interface method is marked with . -* The grain's predicate returns `true`. +- The grain class is marked with . +- The interface method is marked with . +- The grain's predicate returns `true`. -With reentrancy, the following case becomes a valid execution and the possibility of the above deadlock is removed. +With reentrancy, the following case becomes a valid execution, removing the possibility of the deadlock described above. -### Case 3: the grain or method is reentrant +### Case 3: The grain or method is reentrant :::image type="content" source="grain-persistence/media/reentrancy-scheduling-diagram-03.png" alt-text="Re-entrancy scheduling diagram with re-entrant grain or method." lightbox="grain-persistence/media/reentrancy-scheduling-diagram-03.png"::: -In this example, grains *A* and *B* can call each other simultaneously without any potential for request scheduling deadlocks because both grains are *re-entrant*. The following sections provide more details on reentrancy. +In this example, grains *A* and *B* can call each other simultaneously without potential request scheduling deadlocks because both grains are *reentrant*. The following sections provide more details on reentrancy. -### Re-entrant grains +### Reentrant grains -The implementation classes may be marked with the to indicate that different requests may be freely interleaved. +You can mark implementation classes with the to indicate that different requests can be freely interleaved. -In other words, a re-entrant activation might start another request while a previous request hasn't finished processing. Execution is still limited to a single thread, so the activation is still executing one turn at a time, and each turn is executing on behalf of only one of the activation's requests. +In other words, a reentrant activation might start processing another request while a previous request hasn't finished. Execution is still limited to a single thread, so the activation executes one turn at a time, and each turn executes on behalf of only one of the activation's requests. -Re-entrant grain code never runs multiple pieces of grain code in parallel (execution of grain code is always single-threaded), but re-entrant grains **may** see the execution of code for different requests interleaving. That is, the continuation turns from different requests may interleave. +Reentrant grain code never runs multiple pieces of grain code in parallel (execution is always single-threaded), but reentrant grains **might** see the execution of code for different requests interleaving. That is, the continuation turns from different requests might interleave. For example, as shown in the following pseudo-code, consider that `Foo` and `Bar` are two methods of the same grain class: @@ -133,7 +134,7 @@ Task Bar() } ``` -If this grain is marked , the execution of `Foo` and `Bar` may interleave. +If this grain is marked , the execution of `Foo` and `Bar` might interleave. For example, the following order of execution is possible: @@ -142,17 +143,17 @@ That is, the turns from different requests interleave. If the grain wasn't re-entrant, the only possible executions would be: line 1, line 2, line 3, line 4 OR: line 3, line 4, line 1, line 2 (a new request can't start before the previous one finished). -The main tradeoff in choosing between reentrant and nonreentrant grains is the code complexity of making interleaving work correctly, and the difficulty to reason about it. +The main tradeoff when choosing between reentrant and non-reentrant grains is the code complexity required to make interleaving work correctly and the difficulty of reasoning about it. -In a trivial case when the grains are stateless and the logic is simple, fewer (but not too few, so that all the hardware threads are used) re-entrant grains should, in general, be slightly more efficient. +In a trivial case where grains are stateless and the logic is simple, using fewer reentrant grains (but not too few, ensuring all hardware threads are utilized) should generally be slightly more efficient. -If the code is more complex, then a larger number of non-reentrant grains, even if slightly less efficient overall, should save you much grief in figuring out nonobvious interleaving issues. +If the code is more complex, using a larger number of non-reentrant grains, even if slightly less efficient overall, might save you significant trouble in debugging non-obvious interleaving issues. In the end, the answer depends on the specifics of the application. ### Interleaving methods -Grain interface methods marked with , always interleaves any other request and may always be interleaved with any other request, even requests for non-[AlwaysInterleave] methods. +Grain interface methods marked with always interleave any other request and can always be interleaved by any other request, even requests for non-`[AlwaysInterleave]` methods. Consider the following example: @@ -191,11 +192,11 @@ await Task.WhenAll(slowpoke.GoSlow(), slowpoke.GoSlow()); await Task.WhenAll(slowpoke.GoFast(), slowpoke.GoFast(), slowpoke.GoFast()); ``` -Calls to `GoSlow` aren't interleaved, so the total execution time of the two `GoSlow` calls takes around 20 seconds. On the other hand, `GoFast` is marked , and the three calls to it are executed concurrently, completing in approximately 10 seconds total instead of requiring at least 30 seconds to complete. +Calls to `GoSlow` aren't interleaved, so the total execution time of the two `GoSlow` calls is around 20 seconds. On the other hand, `GoFast` is marked . The three calls to it execute concurrently, completing in approximately 10 seconds total instead of requiring at least 30 seconds. ### Readonly methods -When a grain method doesn't modify the grain state, it's safe to execute concurrently with other requests. The indicates that a method doesn't modify the state of a grain. Marking methods as `ReadOnly` allows Orleans to process your request concurrently with other `ReadOnly` requests, which may significantly improve the performance of your app. Consider the following example: +When a grain method doesn't modify the grain state, it's safe to execute concurrently with other requests. The indicates that a method doesn't modify the grain's state. Marking methods as `ReadOnly` allows Orleans to process your request concurrently with other `ReadOnly` requests, which can significantly improve your app's performance. Consider the following example: ```csharp public interface IMyGrain : IGrainWithIntegerKey @@ -207,11 +208,11 @@ public interface IMyGrain : IGrainWithIntegerKey } ``` -The `GetCount` method doesn't modify the grain state, so it's marked with `ReadOnly`. Callers awaiting this method invocation aren't blocked by other `ReadOnly` requests to the grain, and the method returns immediately. +The `GetCount` method doesn't modify the grain state, so it's marked `ReadOnly`. Callers awaiting this method invocation aren't blocked by other `ReadOnly` requests to the grain, and the method returns immediately. ### Call chain reentrancy -If a grain calls a method which on another grain which then calls back into the original grain, the call will result in a deadlock unless the call is reentrant. Reentrancy can be enabled on a per-call-site basis by using *call chain reentrancy*. To enable call chain reentrancy, call the method, which returns a value that allows reentrance from any caller further down the call chain until it is disposed. This includes reentrance from the grain calling the method itself. Consider the following example: +If a grain calls a method on another grain, which then calls back into the original grain, the call results in a deadlock unless the call is reentrant. You can enable reentrancy on a per-call-site basis using *call chain reentrancy*. To enable call chain reentrancy, call the method. This method returns a value allowing reentrance from any caller further down the call chain until the value is disposed. This includes reentrance from the grain calling the method itself. Consider the following example: ```csharp public interface IChatRoomGrain : IGrainWithStringKey @@ -248,17 +249,17 @@ public class UserGrain : Grain, IUserGrain } ``` -In the preceding example, `UserGrain.JoinRoom(roomName)` calls into `ChatRoomGrain.OnJoinRoom(user)`, which tries to call back into `UserGrain.GetDisplayName()` to get the user's display name. Since this call chain involves a cycle, this will result in a deadlock if the `UserGrain` doesn't allow reentrance using any of the supported mechanisms discussed in this article. In this instance, we are using , which allows only `roomGrain` to call back into the `UserGrain`. This grants you fine grained control over where and how reentrancy is enabled. +In the preceding example, `UserGrain.JoinRoom(roomName)` calls `ChatRoomGrain.OnJoinRoom(user)`, which tries to call back into `UserGrain.GetDisplayName()` to get the user's display name. Since this call chain involves a cycle, it results in a deadlock if `UserGrain` doesn't allow reentrance using one of the supported mechanisms discussed in this article. In this instance, we use , which allows only `roomGrain` to call back into `UserGrain`. This gives you fine-grained control over where and how reentrancy is enabled. -If you were to instead prevent the deadlock by annotating the `GetDisplayName()` method declaration on `IUserGrain` with `[AlwaysInterleave]`, you would allow any grain to interleave a `GetDisplayName` call with any other method. Instead, you are allowing *only* `roomGrain` to call methods on our grain and only until `scope` is disposed. +If you were to prevent the deadlock by annotating the `GetDisplayName()` method declaration on `IUserGrain` with `[AlwaysInterleave]` instead, you would allow *any* grain to interleave a `GetDisplayName` call with any other method. By using `AllowCallChainReentrancy`, you allow *only* `roomGrain` to call methods on the `UserGrain`, and only until `scope` is disposed. #### Suppress call chain reentrancy -Call chain reentrance can also be *suppressed* using the method. This has limited usefulness to end developers, but it is important for internal use by libraries which extend Orleans grain functionality, such as [streaming](../streaming/index.md) and [broadcast channels](../streaming/broadcast-channel.md) to ensure that developers retain full control over when call chain reentrancy is enabled. +You can also *suppress* call chain reentrance using the method. This has limited usefulness for end developers but is important for internal use by libraries extending Orleans grain functionality, such as [streaming](../streaming/index.md) and [broadcast channels](../streaming/broadcast-channel.md), to ensure developers retain full control over when call chain reentrancy is enabled. ### Reentrancy using a predicate -Grain classes can specify a predicate to determine interleaving on a call-by-call basis by inspecting the request. The `[MayInterleave(string methodName)]` attribute provides this functionality. The argument to the attribute is the name of a static method within the grain class that accepts an object and returns a `bool` indicating whether or not the request should be interleaved. +Grain classes can specify a predicate to determine interleaving on a call-by-call basis by inspecting the request. The `[MayInterleave(string methodName)]` attribute provides this functionality. The argument to the attribute is the name of a static method within the grain class. This method accepts an object and returns a `bool` indicating whether the request should be interleaved. Here's an example that allows interleaving if the request argument type has the `[Interleave]` attribute: diff --git a/docs/orleans/grains/stateless-worker-grains.md b/docs/orleans/grains/stateless-worker-grains.md index 6778b3ba9fcea..9c328cf75a39c 100644 --- a/docs/orleans/grains/stateless-worker-grains.md +++ b/docs/orleans/grains/stateless-worker-grains.md @@ -1,22 +1,22 @@ --- title: Stateless worker grains description: Learn how to use stateless worker grains in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Stateless worker grains -By default, the Orleans runtime creates no more than one activation of a grain within the cluster. This is the most intuitive expression of the Virtual Actor model with each grain corresponding to an entity with a unique type/identity. However, there are also cases when an application needs to perform functional stateless operations that are not tied to a particular entity in the system. For example, if the client sends requests with compressed payloads that need to be decompressed before they could be routed to the target grain for processing, such decompression/routing logic is not tied to a specific entity in the application, and can easily scale-out. +By default, the Orleans runtime creates no more than one activation of a grain within the cluster. This is the most intuitive expression of the Virtual Actor model, where each grain corresponds to an entity with a unique type/identity. However, sometimes an application needs to perform functional stateless operations not tied to a particular entity in the system. For example, if a client sends requests with compressed payloads that need decompression before routing to the target grain for processing, such decompression/routing logic isn't tied to a specific entity and can easily scale out. -When the is applied to a grain class, it indicates to the Orleans runtime that grains of that class should be treated as stateless worker grains. Stateless worker grains have the following properties that make their execution very different from that of normal grain classes. +When you apply the to a grain class, it indicates to the Orleans runtime that grains of that class should be treated as stateless worker grains. Stateless worker grains have the following properties that make their execution very different from normal grain classes: -1. The Orleans runtime can and will create multiple activations of a stateless worker grain on different silos of the cluster. -1. Stateless worker grains execute requests locally as long as the silo is compatible, and therefore don't incur networking or serialization costs. If the local silo is not compatible, requests are forwarded to a compatible silo. -1. The Orleans Runtime automatically creates additional activations of a stateless worker grain if the already existing ones are busy. -The maximum number of activations of a stateless worker grain the runtime creates per silo is limited by default by the number of CPU cores on the machine, unless specified explicitly by the optional `maxLocalWorkers` argument. -1. Because of 2 and 3, stateless worker grain activations are not individually addressable. Two subsequent requests to a stateless worker grain may be processed by different activations of it. +1. The Orleans runtime can and does create multiple activations of a stateless worker grain on different silos in the cluster. +1. Stateless worker grains execute requests locally as long as the silo is compatible, therefore incurring no networking or serialization costs. If the local silo isn't compatible, requests are forwarded to a compatible silo. +1. The Orleans Runtime automatically creates additional activations of a stateless worker grain if the existing ones are busy. The maximum number of activations per silo is limited by default by the number of CPU cores on the machine, unless you specify it explicitly using the optional `maxLocalWorkers` argument. +1. Because of points 2 and 3, stateless worker grain activations aren't individually addressable. Two subsequent requests to a stateless worker grain might be processed by different activations. -Stateless worker grains provide a straightforward way of creating an auto-managed pool of grain activations that automatically scales up and down based on the actual load. The runtime always scans for available stateless worker grain activations in the same order. Because of that, it always dispatches requests to the first idle local activation it can find and only gets to the last one if all previous activations are busy. If all activations are busy and the activation limit hasn't been reached, it creates one more activation at the end of the list and dispatches the request to it. That means that when the rate of requests to a stateless worker grain increases, and existing activations are all currently busy, the runtime expands the pool of its activations up to the limit. Conversely, when the load drops, and it can be handled by a smaller number of activations of the stateless worker grain, the activations at the tail of the list will not be getting requests dispatched to them. They will become idle, and eventually deactivated by the standard activation collection process. Hence, the pool of activations will eventually shrink to match the load. +Stateless worker grains provide a straightforward way to create an auto-managed pool of grain activations that automatically scales up and down based on the actual load. The runtime always scans for available stateless worker grain activations in the same order. Because of this, it always dispatches requests to the first idle local activation it finds and only proceeds to the last one if all previous activations are busy. If all activations are busy and the activation limit hasn't been reached, it creates one more activation at the end of the list and dispatches the request to it. This means that when the rate of requests to a stateless worker grain increases and existing activations are all busy, the runtime expands the pool of activations up to the limit. Conversely, when the load drops and a smaller number of activations can handle it, the activations at the tail of the list won't receive requests. They become idle and are eventually deactivated by the standard activation collection process. Thus, the pool of activations eventually shrinks to match the load. The following example defines a stateless worker grain class `MyStatelessWorkerGrain` with the default maximum activation number limit. @@ -28,16 +28,14 @@ public class MyStatelessWorkerGrain : Grain, IMyStatelessWorkerGrain } ``` -Making a call to a stateless worker grain is the same as to any other grain. -The only difference is that in most cases a single grain ID is used, for example `0` or . -Multiple grain IDs can be used when having multiple stateless worker grain pools, one per ID is desirable. +Making a call to a stateless worker grain is the same as calling any other grain. The only difference is that in most cases, you use a single grain ID, for example, `0` or . You can use multiple grain IDs if having multiple stateless worker grain pools (one per ID) is desirable. ```csharp var worker = GrainFactory.GetGrain(0); await worker.Process(args); ``` -This one defines a stateless worker grain class with no more than one-grain activation per silo. +This example defines a stateless worker grain class with no more than one activation per silo. ```csharp [StatelessWorker(1)] // max 1 activation per silo @@ -47,21 +45,21 @@ public class MyLonelyWorkerGrain : ILonelyWorkerGrain } ``` -Note that does not change the reentrancy of the target grain class. Just like any other grains, stateless worker grains are non-reentrant by default. They can be explicitly made reentrant by adding a to the grain class. +Note that doesn't change the reentrancy of the target grain class. Like any other grain, stateless worker grains are non-reentrant by default. You can explicitly make them reentrant by adding a to the grain class. ## State -The "stateless" part of "stateless worker" does not mean that a stateless worker cannot have a state and is limited only to executing functional operations. Like any other grain, a stateless worker grain can load and keep in memory any state it needs. It's just because multiple activations of a stateless worker grain can be created on the same and different silos of the cluster, there is no easy mechanism to coordinate state held by different activations. +The "stateless" part of "stateless worker" doesn't mean a stateless worker cannot have state or is limited only to executing functional operations. Like any other grain, a stateless worker grain can load and keep in memory any state it needs. However, because multiple activations of a stateless worker grain can be created on the same and different silos in the cluster, there's no easy mechanism to coordinate state held by different activations. -Several useful patterns involve stateless worker holding state. +Several useful patterns involve stateless workers holding state. -### Scaled out hot cache items +### Scaled-out hot cache items -For hot cache items that experience high throughput, holding each such item in a stateless worker grain makes it: +For hot cache items experiencing high throughput, holding each item in a stateless worker grain provides these benefits: -1. Automatically scale out within a silo and across all silos in the cluster, and; -1. Makes the data always locally available on the silo that received the client request via its client gateway, so that the requests can be answered without an extra network hop to another silo. +1. It automatically scales out within a silo and across all silos in the cluster. +1. It makes the data always locally available on the silo that received the client request via its client gateway, allowing requests to be answered without an extra network hop to another silo. -### Reduce style aggregation +### Reduce-style aggregation -In some scenarios, applications need to calculate certain metrics across all grains of a particular type in the cluster and report the aggregates periodically. Examples are reporting several players per game map, the average duration of a VoIP call, etc. If each of the many thousands or millions of grains were to report their metrics to a single global aggregator, the aggregator would get immediately overloaded unable to process the flood of reports. The alternative approach is to turn this task into a 2 (or more) step to reduce style aggregation. The first layer of aggregation is done by reporting grain sending their metrics to a stateless worker pre-aggregation grain. The Orleans runtime will automatically create multiple activations of the stateless worker grain with each silo. Since all such calls will be processed locally with no remote calls or serialization of the messages, the cost of such aggregation will be significantly less than in a remote case. Now each of the pre-aggregation stateless worker grain activations, independently or in coordination with other local activations, can send their aggregated reports to the global final aggregator (or to another reduction layer if necessary) without overloading it. +In some scenarios, applications need to calculate certain metrics across all grains of a particular type in the cluster and report the aggregates periodically. Examples include reporting the number of players per game map or the average duration of a VoIP call. If each of the many thousands or millions of grains reported their metrics to a single global aggregator, the aggregator would immediately become overloaded and unable to process the flood of reports. The alternative approach is to turn this task into a two-step (or more) reduce-style aggregation. The first layer of aggregation involves the reporting grains sending their metrics to a stateless worker pre-aggregation grain. The Orleans runtime automatically creates multiple activations of the stateless worker grain on each silo. Since Orleans processes all such calls locally without remote calls or message serialization, the cost of this aggregation is significantly less than in a remote case. Now, each pre-aggregation stateless worker grain activation, independently or in coordination with other local activations, can send its aggregated report to the global final aggregator (or to another reduction layer if necessary) without overloading it. diff --git a/docs/orleans/grains/timers-and-reminders.md b/docs/orleans/grains/timers-and-reminders.md index f4c3d29b9edcf..94d0c8a747b1e 100644 --- a/docs/orleans/grains/timers-and-reminders.md +++ b/docs/orleans/grains/timers-and-reminders.md @@ -1,18 +1,19 @@ --- title: Timers and reminders description: Learn how to use timers and reminders in .NET Orleans. -ms.date: 08/01/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Timers and reminders -The Orleans runtime provides two mechanisms, called timers and reminders, that enable the developer to specify periodic behavior for grains. +The Orleans runtime provides two mechanisms, timers and reminders, that enable you to specify periodic behavior for grains. ## Timers -**Timers** are used to create periodic grain behavior that isn't required to span multiple activations (instantiations of the grain). A timer is identical to the standard .NET class. In addition, timers are subject to single-threaded execution guarantees within the grain activation that they operate on. +Use **timers** to create periodic grain behavior that isn't required to span multiple activations (instantiations of the grain). A timer is identical to the standard .NET class. Additionally, timers are subject to single-threaded execution guarantees within the grain activation they operate on. -Each activation may have zero or more timers associated with it. The runtime executes each timer routine within the runtime context of the activation that it's associated with. +Each activation can have zero or more timers associated with it. The runtime executes each timer routine within the runtime context of its associated activation. ## Timer usage @@ -25,38 +26,38 @@ protected IGrainTimer RegisterGrainTimer( GrainTimerCreationOptions options) // timer creation options ``` -To cancel the timer, you dispose of it. +To cancel the timer, dispose of it. -A timer ceases to trigger if the grain is deactivated or when a fault occurs and its silo crashes. +A timer stops triggering if the grain deactivates or when a fault occurs and its silo crashes. ***Important considerations:*** -* When activation collection is enabled, the execution of a timer callback doesn't change the activation's state from idle to in-use. This means that a timer can't be used to postpone the deactivation of otherwise idle activations. -* The period passed to `Grain.RegisterGrainTimer` is the amount of time that passes from the moment the `Task` returned by `callback` is resolved to the moment that the next invocation of `callback` should occur. This not only makes it impossible for successive calls to `callback` to overlap, but also makes it so that the length of time `callback` takes to complete affects the frequency at which `callback` is invoked. This is an important deviation from the semantics of . -* Each invocation of `callback` is delivered to an activation on a separate turn, and never runs concurrently with other turns on the same activation. -* Callbacks do not interleave by default. Interleaving can be enabled by setting Interleave to true on GrainTimerCreationOptions. -* Grain timers can be updated using the Change(TimeSpan, TimeSpan) method on the returned IGrainTimer instance. -* Callbacks can keep the grain active, preventing it from being collected if the timer period is relatively short. This can be enabled by setting KeepAlive to true on GrainTimerCreationOptions. -* Callbacks can receive a CancellationToken which is canceled when the timer is disposed or the grain starts to deactivate. -* Callbacks can dispose the grain timer which fired them. -* Callbacks are subject to grain call filters. -* Callbacks are visible in distributed tracing, when distributed tracing is enabled. -* POCO grains (grain classes which do not inherit from Grain) can register grain timers using the RegisterGrainTimer extension method. +- When activation collection is enabled, executing a timer callback doesn't change the activation's state from idle to in-use. This means you can't use a timer to postpone the deactivation of otherwise idle activations. +- The period passed to `Grain.RegisterGrainTimer` is the amount of time passing from the moment the `Task` returned by `callback` resolves to the moment the next invocation of `callback` should occur. This not only prevents successive calls to `callback` from overlapping but also means the time `callback` takes to complete affects the frequency at which `callback` is invoked. This is an important deviation from the semantics of . +- Each invocation of `callback` is delivered to an activation on a separate turn and never runs concurrently with other turns on the same activation. +- Callbacks don't interleave by default. You can enable interleaving by setting `Interleave` to `true` on `GrainTimerCreationOptions`. +- You can update grain timers using the `Change(TimeSpan, TimeSpan)` method on the returned `IGrainTimer` instance. +- Callbacks can keep the grain active, preventing collection if the timer period is relatively short. Enable this by setting `KeepAlive` to `true` on `GrainTimerCreationOptions`. +- Callbacks can receive a `CancellationToken` that is canceled when the timer is disposed or the grain starts to deactivate. +- Callbacks can dispose of the grain timer that fired them. +- Callbacks are subject to grain call filters. +- Callbacks are visible in distributed tracing when distributed tracing is enabled. +- POCO grains (grain classes that don't inherit from `Grain`) can register grain timers using the `RegisterGrainTimer` extension method. ## Reminders Reminders are similar to timers, with a few important differences: -* Reminders are persistent and continue to trigger in almost all situations (including partial or full cluster restarts) unless explicitly canceled. -* Reminder "definitions" are written to storage. However, each specific occurrence, with its specific time, isn't. This has the side effect that if the cluster is down at the time of a specific reminder tick, it will be missed and only the next tick of the reminder happens. -* Reminders are associated with a grain, not any specific activation. -* If a grain has no activation associated with it when a reminder ticks, the grain is created. If an activation becomes idle and is deactivated, a reminder associated with the same grain reactivates the grain when it ticks next. -* Reminder delivery occurs via message and is subject to the same interleaving semantics as all other grain methods. -* Reminders shouldn't be used for high-frequency timers- their period should be measured in minutes, hours, or days. +- Reminders are persistent and continue to trigger in almost all situations (including partial or full cluster restarts) unless explicitly canceled. +- Reminder "definitions" are written to storage. However, each specific occurrence with its specific time isn't stored. This has the side effect that if the cluster is down when a specific reminder tick is due, it will be missed, and only the next tick of the reminder occurs. +- Reminders are associated with a grain, not any specific activation. +- If a grain has no activation associated with it when a reminder ticks, Orleans creates the grain activation. If an activation becomes idle and is deactivated, a reminder associated with the same grain reactivates the grain when it ticks next. +- Reminder delivery occurs via message and is subject to the same interleaving semantics as all other grain methods. +- You shouldn't use reminders for high-frequency timers; their period should be measured in minutes, hours, or days. ## Configuration -Since reminders are persistent, they rely upon storage to function. You must specify which storage backing to use before the reminder subsystem functions. You do this by configuring one of the reminder providers via `Use{X}ReminderService` extension methods, where `X` is the name of the provider, for example, . +Since reminders are persistent, they rely on storage to function. You must specify which storage backing to use before the reminder subsystem can function. Do this by configuring one of the reminder providers via `Use{X}ReminderService` extension methods, where `X` is the name of the provider (for example, ). Azure Table configuration: @@ -88,7 +89,7 @@ var silo = new HostBuilder() .Build(); ``` - If you just want a placeholder implementation of reminders to work without needing to set up an Azure account or SQL database, then this gives you a development-only implementation of the reminder system: + If you just want a placeholder implementation of reminders to work without needing to set up an Azure account or SQL database, this provides a development-only implementation of the reminder system: ```csharp var silo = new HostBuilder() @@ -104,7 +105,7 @@ var silo = new HostBuilder() ## Reminder usage -A grain that uses reminders must implement the method. +A grain using reminders must implement the method. ```csharp Task IRemindable.ReceiveReminder(string reminderName, TickStatus status) @@ -123,11 +124,11 @@ protected Task RegisterOrUpdateReminder( TimeSpan period) ``` -* `reminderName`: is a string that must uniquely identify the reminder within the scope of the contextual grain. -* `dueTime`: specifies a quantity of time to wait before issuing the first-timer tick. -* `period`: specifies the period of the timer. +- `reminderName`: is a string that must uniquely identify the reminder within the scope of the contextual grain. +- `dueTime`: specifies a quantity of time to wait before issuing the first-timer tick. +- `period`: specifies the period of the timer. -Since reminders survive the lifetime of any single activation, they must be explicitly canceled (as opposed to being disposed). You cancel a reminder by calling : +Since reminders survive the lifetime of any single activation, you must explicitly cancel them (as opposed to disposing of them). Cancel a reminder by calling : ```csharp protected Task UnregisterReminder(IGrainReminder reminder) @@ -135,9 +136,9 @@ protected Task UnregisterReminder(IGrainReminder reminder) The `reminder` is the handle object returned by . -Instances of `IGrainReminder` aren't guaranteed to be valid beyond the lifespan of an activation. If you wish to identify a reminder in a way that persists, use a string containing the reminder's name. +Instances of `IGrainReminder` aren't guaranteed to be valid beyond the lifespan of an activation. If you wish to identify a reminder persistently, use a string containing the reminder's name. -If you only have the reminder's name and need the corresponding instance of `IGrainReminder`, call the method: +If you only have the reminder's name and need the corresponding `IGrainReminder` instance, call the method: ```csharp protected Task GetReminder(string reminderName) @@ -145,30 +146,30 @@ protected Task GetReminder(string reminderName) ## Decide which to use -We recommend that you use timers in the following circumstances: +We recommend using timers in the following circumstances: -* If it doesn't matter (or is desirable) that the timer ceases to function when the activation is deactivated or failures occur. -* The resolution of the timer is small (for example, reasonably expressible in seconds or minutes). -* The timer callback can be started from or when a grain method is invoked. +- If it doesn't matter (or is desirable) that the timer stops functioning when the activation deactivates or failures occur. +- The timer's resolution is small (e.g., reasonably expressible in seconds or minutes). +- You can start the timer callback from or when a grain method is invoked. -We recommend that you use reminders in the following circumstances: +We recommend using reminders in the following circumstances: -* When the periodic behavior needs to survive the activation and any failures. -* Performing infrequent tasks (for example, reasonably expressible in minutes, hours, or days). +- When the periodic behavior needs to survive activation and any failures. +- Performing infrequent tasks (e.g., reasonably expressible in minutes, hours, or days). ## Combine timers and reminders -You might consider using a combination of reminders and timers to accomplish your goal. For example, if you need a timer with a small resolution that needs to survive across activations, you can use a reminder that runs every five minutes, whose purpose is to wake up a grain that restarts a local timer that may have been lost due to deactivation. +You might consider using a combination of reminders and timers to accomplish your goal. For example, if you need a timer with a small resolution that must survive across activations, you can use a reminder running every five minutes. Its purpose would be to wake up a grain that restarts a local timer possibly lost due to deactivation. ## POCO grain registrations -To register a timer or reminder with [a POCO grain](../migration-guide.md#poco-grains-and-igrainbase), you implement the interface and inject the or into the grain's constructor. +To register a timer or reminder with [a POCO grain](../migration-guide.md#poco-grains-and-igrainbase), implement the interface and inject or into the grain's constructor. :::code source="./snippets/timers/PingGrain.cs"::: -The preceding code: +The preceding code does the following: -- Defines a POCO grain that implements , `IPingGrain`, and . -- Registers a timer that is invoked every 10 seconds, and starts 3 seconds after registration. -- When `Ping` is called, registers a reminder that is invoked every hour, and starts immediately following registration. +- Defines a POCO grain implementing , `IPingGrain`, and . +- Registers a timer invoked every 10 seconds, starting 3 seconds after registration. +- When `Ping` is called, registers a reminder invoked every hour, starting immediately after registration. - The `Dispose` method cancels the reminder if it's registered. diff --git a/docs/orleans/grains/transactions.md b/docs/orleans/grains/transactions.md index 8a26775839880..c37236993e58b 100644 --- a/docs/orleans/grains/transactions.md +++ b/docs/orleans/grains/transactions.md @@ -1,21 +1,22 @@ ---- +--- title: Transactions in Orleans description: Learn how to use transactions in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Orleans transactions -Orleans supports distributed ACID transactions against persistent grain state. Transactions are implemented using the [Microsoft.Orleans.Transactions](https://www.nuget.org/packages/Microsoft.Orleans.Transactions) NuGet package. The source code for the sample app in this article is comprised of four projects: +Orleans supports distributed ACID transactions against persistent grain state. Transactions are implemented using the [Microsoft.Orleans.Transactions](https://www.nuget.org/packages/Microsoft.Orleans.Transactions) NuGet package. The source code for the sample app in this article consists of four projects: -- **_Abstractions_**: A class library containing the grain interfaces and shared classes. -- **_Grains_**: A class library containing the grain implementations. -- **_Server_**: A console app that consumes the abstractions and grains class libraries and acts as the Orleans silo. -- **_Client_**: A console app that consumes the abstractions class library that represents the Orleans client. +- **Abstractions**: A class library containing the grain interfaces and shared classes. +- **Grains**: A class library containing the grain implementations. +- **Server**: A console app that consumes the abstractions and grains class libraries and acts as the Orleans silo. +- **Client**: A console app that consumes the abstractions class library that represents the Orleans client. ## Setup -Orleans transactions are opt-in. A silo and client both must be configured to use transactions. If they aren't configured, any calls to transactional methods on a grain implementation will receive the . To enable transactions on a silo, call on the silo host builder: +Orleans transactions are opt-in. Both the silo and the client must be configured to use transactions. If they aren't configured, any calls to transactional methods on a grain implementation receive an . To enable transactions on a silo, call on the silo host builder: ```csharp var builder = Host.CreateDefaultBuilder(args) @@ -37,17 +38,17 @@ var builder = Host.CreateDefaultBuilder(args) ### Transactional state storage -To use transactions, you need to configure a data store. To support various data stores with transactions, the storage abstraction is used. This abstraction is specific to the needs of transactions, unlike generic grain storage (). To use transaction-specific storage, configure the silo using any implementation of `ITransactionalStateStorage`, such as Azure (). +To use transactions, you need to configure a data store. To support various data stores with transactions, Orleans uses the storage abstraction . This abstraction is specific to the needs of transactions, unlike generic grain storage (). To use transaction-specific storage, configure the silo using any implementation of `ITransactionalStateStorage`, such as Azure (). For example, consider the following host builder configuration: :::code source="snippets/transactions/Server/Program.cs"::: -For development purposes, if transaction-specific storage is not available for the datastore you need, you can use an `IGrainStorage` implementation instead. For any transactional state that doesn't have a store configured, transactions will attempt to fail over to the grain storage using a bridge. Accessing a transactional state via a bridge to grain storage is less efficient and may not be supported in the future. Hence, the recommendation is to only use this for development purposes. +For development purposes, if transaction-specific storage isn't available for the datastore you need, you can use an `IGrainStorage` implementation instead. For any transactional state without a configured store, transactions attempt to fail over to the grain storage using a bridge. Accessing transactional state via a bridge to grain storage is less efficient and might not be supported in the future. Therefore, we recommend using this approach only for development purposes. ## Grain interfaces -For a grain to support transactions, transactional methods on a grain interface must be marked as being part of a transaction using the . The attribute needs to indicate how the grain call behaves in a transactional environment as detailed with the following values: +For a grain to support transactions, you must mark transactional methods on its grain interface as part of a transaction using the . The attribute needs to indicate how the grain call behaves in a transactional environment, as detailed by the following values: - : Call is transactional and will always create a new transaction context (it starts a new transaction), even if called within an existing transaction context. - : Call is transactional but can only be called within the context of an existing transaction. @@ -56,17 +57,17 @@ For a grain to support transactions, transactional methods on a grain interface - : Call is not transactional but supports transactions. If called within the context of a transaction, the context will be passed to the call. - : Call is not transactional and cannot be called from within a transaction. If called within the context of a transaction, it will throw the . -Calls can be marked as `TransactionOption.Create`, meaning the call will always start its transaction. For example, the `Transfer` operation in the ATM grain below will always start a new transaction that involves the two referenced accounts. +You can mark calls as `TransactionOption.Create`, meaning the call always starts its transaction. For example, the `Transfer` operation in the ATM grain below always starts a new transaction involving the two referenced accounts. :::code source="snippets/transactions/Abstractions/IAtmGrain.cs"::: -The transactional operations `Withdraw` and `Deposit` on the account grain are marked `TransactionOption.Join`, indicating that they can only be called within the context of an existing transaction, which would be the case if they were called during `IAtmGrain.Transfer`. The `GetBalance` call is marked `CreateOrJoin` so it can be called from within an existing transaction, like via `IAtmGrain.Transfer`, or on its own. +The transactional operations `Withdraw` and `Deposit` on the account grain are marked `TransactionOption.Join`. This indicates they can only be called within the context of an existing transaction, which would be the case if called during `IAtmGrain.Transfer`. The `GetBalance` call is marked `CreateOrJoin`, so you can call it either from within an existing transaction (like via `IAtmGrain.Transfer`) or on its own. :::code source="snippets/transactions/Abstractions/IAccountGrain.cs"::: ### Important considerations -The `OnActivateAsync` couldn't be marked as transactional as any such call requires a proper setup before the call. It exists only for the grain application API. This means that an attempt to read transactional state as part of these methods will throw an exception in the runtime. +You cannot mark `OnActivateAsync` as transactional because any such call requires proper setup before the call. It exists only for the grain application API. This means attempting to read transactional state as part of these methods throws an exception in the runtime. ## Grain implementations @@ -84,7 +85,7 @@ public interface ITransactionalState } ``` -All read or write access to the persisted state must be performed via synchronous functions passed to the transactional state facet. This allows the transaction system to perform or cancel these operations transactionally. To use a transactional state within a grain, you define a serializable state class to be persisted and declare the transactional state in the grain's constructor with a . The latter declares the state name and, optionally, which transactional state storage to use. For more information, see [Setup](#setup). +Perform all read or write access to the persisted state via synchronous functions passed to the transactional state facet. This allows the transaction system to perform or cancel these operations transactionally. To use transactional state within a grain, define a serializable state class to be persisted and declare the transactional state in the grain's constructor using a . This attribute declares the state name and, optionally, which transactional state storage to use. For more information, see [Setup](#setup). ```csharp [AttributeUsage(AttributeTargets.Parameter)] @@ -113,11 +114,11 @@ The `Balance` state object is then used in the `AccountGrain` implementation as > [!IMPORTANT] > A transactional grain must be marked with the to ensure that the transaction context is correctly passed to the grain call. -In the preceding example, the is used to declare that the `balance` constructor parameter should be associated with a transactional state named `"balance"`. With this declaration, Orleans will inject an instance with a state loaded from the transactional state storage named `"TransactionStore"`. The state can be modified via `PerformUpdate` or read via `PerformRead`. The transaction infrastructure will ensure that any such changes performed as part of a transaction, even among multiple grains distributed over an Orleans cluster, will either all be committed or all be undone upon completion of the grain call that created the transaction (`IAtmGrain.Transfer` in the preceding example). +In the preceding example, the declares that the `balance` constructor parameter should be associated with a transactional state named `"balance"`. With this declaration, Orleans injects an instance with state loaded from the transactional state storage named `"TransactionStore"`. You can modify the state via `PerformUpdate` or read it via `PerformRead`. The transaction infrastructure ensures that any such changes performed as part of a transaction (even among multiple grains distributed across an Orleans cluster) are either all committed or all undone upon completion of the grain call that created the transaction (`IAtmGrain.Transfer` in the preceding example). ## Call transaction methods from a client -The recommended way to call a transaction grain method is to use the `ITransactionClient`. The `ITransactionClient` is automatically registered with the dependency injection service provider when the Orleans client is configured. The `ITransactionClient` is used to create a transaction context and to call transactional grain methods within that context. The following example shows how to use the `ITransactionClient` to call transactional grain methods. +The recommended way to call a transactional grain method is to use the `ITransactionClient`. Orleans automatically registers `ITransactionClient` with the dependency injection service provider when you configure the Orleans client. Use `ITransactionClient` to create a transaction context and call transactional grain methods within that context. The following example shows how to use `ITransactionClient` to call transactional grain methods. :::code source="snippets/transactions/Client/Program.cs" highlight="11-12,30-31,38-44"::: @@ -131,17 +132,17 @@ In the preceding client code: - `Withdraw` on the `from` account grain reference. - `Deposit` on the `to` account grain reference. -Transactions are always committed unless there is an exception that is thrown in the `transactionDelegate` or a contradictory `transactionOption` specified. While the recommended way to call transactional grain methods is to use the `ITransactionClient`, you can also call transactional grain methods directly from another grain. +Transactions are always committed unless an exception is thrown in the `transactionDelegate` or a contradictory `transactionOption` is specified. While using `ITransactionClient` is the recommended way to call transactional grain methods, you can also call them directly from another grain. ## Call transaction methods from another grain -Transactional methods on a grain interface are called like any other grain method. As an alternative approach using the `ITransactionClient`, the `AtmGrain` implementation below calls the `Transfer` method (which is transactional) on the `IAccountGrain` interface. +Call transactional methods on a grain interface like any other grain method. As an alternative to using `ITransactionClient`, the `AtmGrain` implementation below calls the `Transfer` method (which is transactional) on the `IAccountGrain` interface. Consider the `AtmGrain` implementation, which resolves the two referenced account grains and makes the appropriate calls to `Withdraw` and `Deposit`: :::code source="snippets/transactions/Grains/AtmGrain.cs"::: -Your client app code can call `AtmGrain.Transfer` in a transactional manner as follows: +Your client app code can call `AtmGrain.Transfer` transactionally as follows: ```csharp IAtmGrain atmOne = client.GetGrain(0); @@ -157,6 +158,6 @@ uint toBalance = await client.GetGrain(to).GetBalance(); In the preceding calls, an `IAtmGrain` is used to transfer 100 units of currency from one account to another. After the transfer is complete, both accounts are queried to get their current balance. The currency transfer, as well as both account queries, are performed as ACID transactions. -As shown in the preceding example, transactions can return values within a `Task`, like other grain calls. But upon call failure, they will not throw application exceptions but rather an or . If the application throws an exception during the transaction and that exception causes the transaction to fail (as opposed to failing because of other system failures), the application exception will be the inner exception of the `OrleansTransactionException`. +As shown in the preceding example, transactions can return values within a `Task`, like other grain calls. However, upon call failure, they don't throw application exceptions but rather an or . If the application throws an exception during the transaction, and that exception causes the transaction to fail (as opposed to failing due to other system failures), the application exception becomes the inner exception of the `OrleansTransactionException`. -If a transaction exception is thrown of type , the transaction failed and can be retried. Any other exception thrown indicates that the transaction terminated with an unknown state. Since transactions are distributed operations, a transaction in an unknown state could have succeeded, failed, or still be in progress. For this reason, it's advisable to allow a call timeout period () to pass, to avoid cascading aborts, before verifying the state or retrying the operation. +If a transaction exception of type is thrown, the transaction failed and can be retried. Any other exception thrown indicates the transaction terminated with an unknown state. Since transactions are distributed operations, a transaction in an unknown state could have succeeded, failed, or still be in progress. For this reason, it's advisable to allow a call timeout period () to pass before verifying the state or retrying the operation to avoid cascading aborts. diff --git a/docs/orleans/host/client.md b/docs/orleans/host/client.md index 3be2d2159f34e..932aaf944aa99 100644 --- a/docs/orleans/host/client.md +++ b/docs/orleans/host/client.md @@ -1,30 +1,31 @@ --- title: Orleans clients description: Learn how to write .NET Orleans clients. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual zone_pivot_groups: orleans-version --- # Orleans clients -A client allows non-grain code to interact with an Orleans cluster. Clients allow application code to communicate with grains and streams hosted in a cluster. There are two ways to obtain a client, depending on where you host the client code: in the same process as a silo, or in a separate process. This article discusses both options, starting with the recommended option: co-hosting the client code in the same process as the grain code. +A client allows non-grain code to interact with an Orleans cluster. Clients enable application code to communicate with grains and streams hosted in a cluster. There are two ways to obtain a client, depending on where you host the client code: in the same process as a silo or in a separate process. This article discusses both options, starting with the recommended approach: co-hosting client code in the same process as grain code. ## Co-hosted clients -If you host the client code in the same process as the grain code, then you can directly obtain the client from the hosting application's dependency injection container. In this case, the client communicates directly with the silo it is attached to and can take advantage of the extra knowledge that the silo has about the cluster. +If you host client code in the same process as grain code, you can directly obtain the client from the hosting application's dependency injection container. In this case, the client communicates directly with the silo it's attached to and can take advantage of the silo's extra knowledge about the cluster. -This provides several benefits, including reducing network and CPU overhead as well as decreasing latency and increasing throughput and reliability. The client utilizes the silo's knowledge of the cluster topology and state and does not need to use a separate gateway. This avoids a network hop and serialization/deserialization round trip. This therefore also increases reliability, since the number of required nodes in between the client and the grain is minimized. If the grain is a [stateless worker grain](../grains/stateless-worker-grains.md) or otherwise happens to be activated on the silo where the client is hosted, then no serialization or network communication needs to be performed at all and the client can reap the additional performance and reliability gains. Co-hosting client and grain code also simplifies deployment and application topology by eliminating the need for two distinct application binaries to be deployed and monitored. +This approach provides several benefits, including reduced network and CPU overhead, decreased latency, and increased throughput and reliability. The client uses the silo's knowledge of the cluster topology and state and doesn't need a separate gateway. This avoids a network hop and a serialization/deserialization round trip, thereby increasing reliability by minimizing the number of required nodes between the client and the grain. If the grain is a [stateless worker grain](../grains/stateless-worker-grains.md) or happens to be activated on the same silo where the client is hosted, no serialization or network communication is needed at all, allowing the client to achieve additional performance and reliability gains. Co-hosting client and grain code also simplifies deployment and application topology by eliminating the need to deploy and monitor two distinct application binaries. -There are also detractors to this approach, primarily that the grain code is no longer isolated from the client process. Therefore, issues in client code, such as blocking IO or lock contention causing thread starvation can affect the performance of grain code. Even without code defects like the aforementioned, *noisy neighbor* effects can result simply by having the client code execute on the same processor as grain code, putting additional strain on CPU cache and additional contention for local resources in general. Additionally, identifying the source of these issues is now more difficult because monitoring systems cannot distinguish what is logically client code from grain code. +There are also drawbacks to this approach, primarily that the grain code is no longer isolated from the client process. Therefore, issues in client code, such as blocking I/O or lock contention causing thread starvation, can affect grain code performance. Even without such code defects, *noisy neighbor* effects can occur simply because client code executes on the same processor as grain code, putting additional strain on the CPU cache and increasing contention for local resources. Additionally, identifying the source of these issues becomes more difficult because monitoring systems cannot distinguish logically between client code and grain code. -Despite these detractors, co-hosting client code with grain code is a popular option and the recommended approach for most applications. To elaborate, the aforementioned detractors are minimal in practice for the following reasons: +Despite these drawbacks, co-hosting client code with grain code is a popular option and the recommended approach for most applications. The aforementioned drawbacks are often minimal in practice for the following reasons: -* Client code is often very *thin*, for example, translating incoming HTTP requests into grain calls, and therefore the *noisy neighbor* effects are minimal and comparable in cost to the otherwise required gateway. -* If a performance issue arises, the typical workflow for a developer involves tools such as CPU profilers and debuggers, which are still effective in quickly identifying the source of the issue despite having both client and grain code executing in the same process. In other words, metrics become more coarse and less able to precisely identify the source of an issue, but more detailed tools are still effective. +- Client code is often very *thin* (e.g., translating incoming HTTP requests into grain calls). Therefore, *noisy neighbor* effects are minimal and comparable in cost to the otherwise required gateway. +- If a performance issue arises, your typical workflow likely involves tools such as CPU profilers and debuggers. These tools remain effective in quickly identifying the source of the issue, even with both client and grain code executing in the same process. In other words, while metrics become coarser and less able to precisely identify the issue's source, more detailed tools are still effective. ### Obtain a client from a host -If hosting using the [.NET Generic Host](../../core/extensions/generic-host.md), the client will be available in the host's [dependency injection](../../core/extensions/dependency-injection.md) container automatically and can be injected into services such as [ASP.NET controllers](/aspnet/core/mvc/controllers/actions) or implementations. +If you host using the [.NET Generic Host](../../core/extensions/generic-host.md), the client is automatically available in the host's [dependency injection](../../core/extensions/dependency-injection.md) container. You can inject it into services such as [ASP.NET controllers](/aspnet/core/mvc/controllers/actions) or implementations. :::zone target="docs" pivot="orleans-7-0" @@ -36,7 +37,7 @@ If hosting using the [.NET Generic Host](../../core/extensions/generic-host.md), :::zone target="docs" pivot="orleans-3-x" -Alternatively, a client interface such as or can be obtained from : +Alternatively, you can obtain a client interface like or from : :::zone-end @@ -47,26 +48,26 @@ await client.GetGrain(0).Ping(); ## External clients -Client code can run outside of the Orleans cluster where grain code is hosted. Hence, an external client acts as a connector or conduit to the cluster and all grains of the application. Usually, clients are used on the frontend web servers to connect to an Orleans cluster that serves as a middle tier with grains executing business logic. +Client code can run outside the Orleans cluster where grain code is hosted. In this case, an external client acts as a connector or conduit to the cluster and all the application's grains. Typically, you use clients on frontend web servers to connect to an Orleans cluster serving as a middle tier, with grains executing business logic. -In a typical setup, a frontend webserver: +In a typical setup, a frontend web server: -* Receives a web request. -* Performs necessary authentication and authorization validation. -* Decides which grain(s) should process the request. -* Uses the [Microsoft.Orleans.Client](https://www.nuget.org/packages/Microsoft.Orleans.Client) NuGet package to make one or more method call to the grain(s). -* Handles successful completion or failures of the grain calls and any returned values. -* Sends a response to the web request. +- Receives a web request. +- Performs necessary authentication and authorization validation. +- Decides which grain(s) should process the request. +- Uses the [Microsoft.Orleans.Client](https://www.nuget.org/packages/Microsoft.Orleans.Client) NuGet package to make one or more method calls to the grain(s). +- Handles successful completion or failures of the grain calls and any returned values. +- Sends a response to the web request. -### Initialization of grain client +### Initialize a grain client -Before a grain client can be used for making calls to grains hosted in an Orleans cluster, it needs to be configured, initialized, and connected to the cluster. +Before you can use a grain client to make calls to grains hosted in an Orleans cluster, you need to configure, initialize, and connect it to the cluster. :::zone target="docs" pivot="orleans-7-0" -Configuration is provided via and several supplemental option classes that contain a hierarchy of configuration properties for programmatically configuring a client. For more information, see [Client configuration](configuration-guide/client-configuration.md). +Provide configuration via and several supplemental option classes containing a hierarchy of configuration properties for programmatically configuring a client. For more information, see [Client configuration](configuration-guide/client-configuration.md). Consider the following example of a client configuration: @@ -88,7 +89,7 @@ using IHost host = new HostBuilder() .Build(); ``` -When the `host` is started, the client will be configured and available through its constructed service provider instance. +When you start the `host`, the client is configured and available through its constructed service provider instance. :::zone-end @@ -96,7 +97,7 @@ When the `host` is started, the client will be configured and available through :::zone target="docs" pivot="orleans-3-x" -Configuration is provided via and several supplemental option classes that contain a hierarchy of configuration properties for programmatically configuring a client. For more information, see [Client configuration](configuration-guide/client-configuration.md). +Provide configuration via and several supplemental option classes containing a hierarchy of configuration properties for programmatically configuring a client. For more information, see [Client configuration](configuration-guide/client-configuration.md). Example of a client configuration: @@ -114,7 +115,7 @@ var client = new ClientBuilder() .Build(); ``` -Lastly, you need to call `Connect()` method on the constructed client object to make it connect to the Orleans cluster. It's an asynchronous method that returns a `Task`. So you need to wait for its completion with an `await` or `.Wait()`. +Finally, you need to call the `Connect()` method on the constructed client object to connect it to the Orleans cluster. It's an asynchronous method returning a `Task`, so you need to wait for its completion using `await` or `.Wait()`. ```csharp await client.Connect(); @@ -124,7 +125,7 @@ await client.Connect(); ### Make calls to grains -Making calls to grain from a client is no different from [making such calls from within grain code](../grains/index.md). The same method, where `T` is the target grain interface, is used in both cases [to obtain grain references](../grains/grain-references.md). The difference is in what factory object is invoked . In client code, you do that through the connected client object as the following example shows: +Making calls to grains from a client is no different from [making such calls from within grain code](../grains/index.md). Use the same method (where `T` is the target grain interface) in both cases [to obtain grain references](../grains/grain-references.md). The difference lies in which factory object invokes . In client code, you do this through the connected client object, as the following example shows: ```csharp IPlayerGrain player = client.GetGrain(playerId); @@ -133,17 +134,17 @@ Task joinGameTask = player.JoinGame(game) await joinGameTask; ``` -A call to a grain method returns a or a as required by the [grain interface rules](../grains/index.md). The client can use the `await` keyword to asynchronously await the returned `Task` without blocking the thread or in some cases the `Wait()` method to block the current thread of execution. +A call to a grain method returns a or , as required by the [grain interface rules](../grains/index.md). The client can use the `await` keyword to asynchronously await the returned `Task` without blocking the thread, or in some cases, use the `Wait()` method to block the current thread of execution. -The major difference between making calls to grains from client code and from within another grain is the single-threaded execution model of grains. Grains are constrained to be single-threaded by the Orleans runtime, while clients may be multi-threaded. Orleans does not provide any such guarantee on the client-side, and so it is up to the client to manage its concurrency using whatever synchronization constructs are appropriate for its environment—locks, events, and `Tasks`. +The major difference between making calls to grains from client code and from within another grain is the single-threaded execution model of grains. The Orleans runtime constrains grains to be single-threaded, while clients can be multi-threaded. Orleans doesn't provide any such guarantee on the client-side, so it's up to the client to manage its concurrency using appropriate synchronization constructs for its environment—locks, events, `Tasks`, etc. ### Receive notifications -There are situations in which a simple request-response pattern is not enough, and the client needs to receive asynchronous notifications. For example, a user might want to be notified when a new message has been published by someone that she is following. +Sometimes, a simple request-response pattern isn't sufficient, and the client needs to receive asynchronous notifications. For example, a user might want notification when someone they follow publishes a new message. -The use of [Observers](../grains/observers.md) is one such mechanism that enables exposing client-side objects as grain-like targets to get invoked by grains. Calls to observers do not provide any indication of success or failure, as they are sent as a one-way best effort message. So it is the responsibility of the application code to build a higher-level reliability mechanism on top of observers where necessary. +Using [Observers](../grains/observers.md) is one mechanism enabling exposure of client-side objects as grain-like targets to be invoked by grains. Calls to observers don't provide any indication of success or failure, as they are sent as one-way, best-effort messages. Therefore, it's the responsibility of your application code to build a higher-level reliability mechanism on top of observers where necessary. -Another mechanism that can be used for delivering asynchronous messages to clients is [Streams](../streaming/index.md). Streams expose indications of success or failure of delivery of individual messages, and hence enable reliable communication back to the client. +Another mechanism for delivering asynchronous messages to clients is [Streams](../streaming/index.md). Streams expose indications of success or failure for individual message delivery, enabling reliable communication back to the client. ### Client connectivity @@ -153,10 +154,10 @@ Another mechanism that can be used for delivering asynchronous messages to clien There are two scenarios in which a cluster client can experience connectivity issues: -* When the client attempts to connect to a silo. -* When making calls on grain references that were obtained from a connected cluster client. +- When the client attempts to connect to a silo. +- When making calls on grain references obtained from a connected cluster client. -In the first case, the client will attempt to connect to a silo. If the client is unable to connect to any silo, it will throw an exception to indicate what went wrong. You can register an to handle the exception and decide whether to retry or not. If no retry filter is provided, or if the retry filter returns `false`, the client gives up for good. +In the first case, the client attempts to connect to a silo. If the client cannot connect to any silo, it throws an exception indicating what went wrong. You can register an to handle the exception and decide whether to retry. If you provide no retry filter, or if the retry filter returns `false`, the client gives up permanently. :::code source="snippets/ClientConnectRetryFilter.cs"::: @@ -168,12 +169,12 @@ In the first case, the client will attempt to connect to a silo. If the client i There are two scenarios in which a cluster client can experience connectivity issues: -* When the method is called initially. -* When making calls on grain references that were obtained from a connected cluster client. +- When the method is called initially. +- When making calls on grain references obtained from a connected cluster client. -In the first case, the `Connect` method will throw an exception to indicate what went wrong. This is typically (but not necessarily) a . If this happens, the cluster client instance is unusable and should be disposed of. A retry filter function can optionally be provided to the `Connect` method which could, for instance, wait for a specified duration before making another attempt. If no retry filter is provided, or if the retry filter returns `false`, the client gives up for good. +In the first case, the `Connect` method throws an exception indicating what went wrong. This is typically (but not necessarily) a . If this happens, the cluster client instance is unusable and should be disposed of. You can optionally provide a retry filter function to the `Connect` method, which could, for instance, wait for a specified duration before making another attempt. If you provide no retry filter, or if the retry filter returns `false`, the client gives up permanently. -If `Connect` returns successfully, the cluster client is guaranteed to be usable until it is disposed of. This means that even if the client experiences connection issues, it will attempt to recover indefinitely. The exact recovery behavior can be configured on a object provided by the , e.g.: +If `Connect` returns successfully, the cluster client is guaranteed to be usable until disposed. This means that even if the client experiences connection issues, it attempts to recover indefinitely. You can configure the exact recovery behavior on a object provided by the , e.g.: ```csharp var client = new ClientBuilder() @@ -186,15 +187,15 @@ var client = new ClientBuilder() :::zone-end -In the second case, where a connection issue occurs during a grain call, a will be thrown on the client-side. This could be handled like so: +In the second case, where a connection issue occurs during a grain call, a is thrown on the client-side. You could handle this like so: :::code source="snippets/Program.cs" id="siloexc"::: -The grain reference is not invalidated in this situation; the call could be retried on the same reference later when a connection might have been re-established. +The grain reference isn't invalidated in this situation; you could retry the call on the same reference later when a connection might have been re-established. ### Dependency injection -The recommended way to create an external client in a program that uses the .NET Generic Host is to inject an singleton instance via dependency injection, which can then be accepted as a constructor parameter in hosted services, ASP.NET controllers, and so on. +The recommended way to create an external client in a program using the .NET Generic Host is to inject an singleton instance via dependency injection. This instance can then be accepted as a constructor parameter in hosted services, ASP.NET controllers, etc. > [!NOTE] > When co-hosting an Orleans silo in the same process that will be connecting to it, it is *not* necessary to manually create a client; Orleans will automatically provide one and manage its lifetime appropriately. @@ -240,7 +241,7 @@ public class ClusterClientHostedService : IHostedService :::zone-end -The service is then registered like this: +Register the service like this: ```csharp await Host.CreateDefaultBuilder(args) @@ -257,7 +258,7 @@ await Host.CreateDefaultBuilder(args) ### Example -Here is an extended version of the example given above of a client application that connects to Orleans, finds the player account, subscribes for updates to the game session the player is part of with an observer, and prints out notifications until the program is manually terminated. +Here's an extended version of the previous example showing a client application that connects to Orleans, finds the player account, subscribes for updates to the game session the player is part of using an observer, and prints notifications until the program is manually terminated. :::zone target="docs" pivot="orleans-7-0" diff --git a/docs/orleans/host/configuration-guide/activation-collection.md b/docs/orleans/host/configuration-guide/activation-collection.md index 48e38420a114e..0df3a53d9a1c4 100644 --- a/docs/orleans/host/configuration-guide/activation-collection.md +++ b/docs/orleans/host/configuration-guide/activation-collection.md @@ -1,7 +1,8 @@ --- title: Activation collection description: Learn about activation collection in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual zone_pivot_groups: orleans-version --- @@ -18,52 +19,52 @@ This article applies to: ✔️ Orleans 7.x and later versions This article applies to: ✔️ Orleans 3.x and earlier versions :::zone-end -A *grain activation* is an in-memory instance of a grain class that gets automatically created by the Orleans runtime on an as-needed basis as a temporary physical embodiment of a grain. +A *grain activation* is an in-memory instance of a grain class that the Orleans runtime automatically creates on an as-needed basis as a temporary physical embodiment of a grain. -Activation collection is the process of removal from memory of unused grain activations. It's conceptually similar to how garbage collection of memory works in .NET. However, activation collection only takes into consideration how long a particular grain activation has been idle. Memory usage isn't used as a factor. +Activation collection is the process of removing unused grain activations from memory. It's conceptually similar to how garbage collection works in .NET. However, activation collection only considers how long a particular grain activation has been idle. Memory usage isn't used as a factor. ## How activation collection works -The general process of activation collection involves Orleans runtime in a silo periodically scanning for grain activations that haven't been used at all for the configured period (Collection Age Limit). Once a grain activation has been idle for that long, it gets deactivated. The deactivation process begins by the runtime calling the grain's method, and completes by removing references to the grain activation object from all data structures of the silo, so that the memory is reclaimed by the .NET GC. +The general process of activation collection involves the Orleans runtime in a silo periodically scanning for grain activations that haven't been used for the configured period (Collection Age Limit). Once a grain activation has been idle for that long, it gets deactivated. The deactivation process begins with the runtime calling the grain's method and completes by removing references to the grain activation object from all silo data structures, allowing the .NET GC to reclaim the memory. -As a result, with no burden put on the application code, only recently used grain activations stay in memory while activations that aren't used anymore get automatically removed, and system resources used by them get reclaimed by the runtime. +As a result, without burdening your application code, only recently used grain activations stay in memory. Activations no longer in use are automatically removed, and the runtime reclaims the system resources they used. -**What counts as "being active" for grain activation collection** +**What counts as "being active" for grain activation collection:** -* Receiving a method call. -* Receiving a reminder. -* Receiving an event via streaming. +- Receiving a method call. +- Receiving a reminder. +- Receiving an event via streaming. -**What does NOT count as "being active" for grain activation collection** +**What does NOT count as "being active" for grain activation collection:** -* Performing a call (to another grain or an Orleans client). -* Timer events. -* Arbitrary IO operations or external calls not involving Orleans framework. +- Performing a call (to another grain or an Orleans client). +- Timer events. +- Arbitrary I/O operations or external calls not involving the Orleans framework. -**Collection Age Limit** +**Collection age limit** :::zone target="docs" pivot="orleans-7-0" -This time after which an idle grain activation becomes subject to collection is called Collection Age Limit. The default Collection Age Limit is 15 minutes, but it can be changed globally or for individual grain classes. +The time after which an idle grain activation becomes subject to collection is called the Collection Age Limit. The default Collection Age Limit is 15 minutes, but you can change it globally or for individual grain classes. :::zone-end :::zone target="docs" pivot="orleans-3-x" -This time after which an idle grain activation becomes subject to collection is called Collection Age Limit. The default Collection Age Limit is 2 hours, but it can be changed globally or for individual grain classes. +The time after which an idle grain activation becomes subject to collection is called the Collection Age Limit. The default Collection Age Limit is 2 hours, but you can change it globally or for individual grain classes. :::zone-end ## Explicit control of activation collection ### Delay activation collection -A grain activation can delay its own collection, by calling method: +A grain activation can delay its collection by calling the method: ```csharp protected void DelayDeactivation(TimeSpan timeSpan) ``` -This call ensures that this activation isn't deactivated for at least the specified time duration. It takes priority over activation collection settings specified in the config, but doesn't cancel them. Therefore, this call provides another hook to **delay the deactivation beyond what is specified in the activation collection settings**. This call can't be used to expedite activation collection. +This call ensures this activation isn't deactivated for at least the specified time duration. It takes priority over activation collection settings specified in the configuration but doesn't cancel them. Therefore, this call provides another hook to **delay deactivation beyond what's specified in the activation collection settings**. You can't use this call to expedite activation collection. A positive `timeSpan` value means "prevent collection of this activation for that time." @@ -71,34 +72,34 @@ A negative `timeSpan` value means "cancel the previous setting of the `DelayDeac **Scenarios:** -1. Activation collection settings specify an age limit of 10 minutes and the grain is making a call to `DelayDeactivation(TimeSpan.FromMinutes(20))`, which causes this activation to not be collected for at least 20 min. +1. Activation collection settings specify an age limit of 10 minutes, and the grain calls `DelayDeactivation(TimeSpan.FromMinutes(20))`. This causes the activation not to be collected for at least 20 minutes. -1. Activation collection settings specify an age limit of 10 minutes and the grain is making a call to `DelayDeactivation(TimeSpan.FromMinutes(5))`, the activation will be collected after 10 min, if no extra calls were made. +1. Activation collection settings specify an age limit of 10 minutes, and the grain calls `DelayDeactivation(TimeSpan.FromMinutes(5))`. The activation will be collected after 10 minutes if no further calls are made. -1. Activation collection settings specify an age limit of 10 minutes and the grain is making a call to `DelayDeactivation(TimeSpan.FromMinutes(5))`, and after 7 minutes there's another call on this grain, the activation will be collected after 17 min from time zero if no extra calls were made. +1. Activation collection settings specify an age limit of 10 minutes, and the grain calls `DelayDeactivation(TimeSpan.FromMinutes(5))`. After 7 minutes, another call arrives for this grain. The activation will be collected after 17 minutes from time zero if no further calls are made. -1. Activation collection settings specify an age limit of 10 minutes and the grain is making a call to `DelayDeactivation(TimeSpan.FromMinutes(20))`, and after 7 minutes there's another call on this grain, the activation will be collected after 20 min from time zero if no extra calls were made. +1. Activation collection settings specify an age limit of 10 minutes, and the grain calls `DelayDeactivation(TimeSpan.FromMinutes(20))`. After 7 minutes, another call arrives for this grain. The activation will be collected after 20 minutes from time zero if no further calls are made. -The `DelayDeactivation` doesn't 100% guarantee that the grain activation won't be deactivated before the specified time expires. Certain failure cases may cause 'premature' deactivation of grains. That means that `DelayDeactivation` **can not be used as a means to 'pin' a grain activation in memory forever or to a specific silo**. `DelayDeactivation` is merely an optimization mechanism that can help reduce the aggregate cost of a grain getting deactivated and reactivated over time. In most cases, there should be no need to use `DelayDeactivation` at all. +`DelayDeactivation` doesn't 100% guarantee the grain activation won't be deactivated before the specified time expires. Certain failure cases might cause 'premature' deactivation of grains. This means `DelayDeactivation` **cannot be used as a means to 'pin' a grain activation in memory forever or to a specific silo**. `DelayDeactivation` is merely an optimization mechanism that can help reduce the aggregate cost of a grain being deactivated and reactivated over time. In most cases, you shouldn't need to use `DelayDeactivation` at all. ### Expedite activation collection -A grain activation can also instruct the runtime to deactivate it the next time it becomes idle by calling method: +A grain activation can also instruct the runtime to deactivate it the next time it becomes idle by calling the method: ```csharp protected void DeactivateOnIdle() ``` -A grain activation is considered idle if it isn't processing any message at the moment. If you call `DeactivateOnIdle` while a grain is processing a message, it gets deactivated as soon as the processing of the current message is finished. If there are any requests queued for the grain, they'll be forwarded to the next activation. +A grain activation is considered idle if it isn't processing any messages at the moment. If you call `DeactivateOnIdle` while a grain is processing a message, it deactivates as soon as the processing of the current message finishes. If any requests are queued for the grain, they'll be forwarded to the next activation. -`DeactivateOnIdle` takes priority over any activation collection settings specified in the config or `DelayDeactivation`. +`DeactivateOnIdle` takes priority over any activation collection settings specified in the configuration or `DelayDeactivation`. > [!NOTE] -> Setting only applies to the grain activation from which it has been called and it does not apply to other grain activation of this type. +> This setting only applies to the specific grain activation from which it was called; it doesn't apply to other activations of this grain type. ## Configuration -Activation collection can be configured using the : +Configure activation collection using : ```csharp mySiloHostBuilder.Configure(options => @@ -114,7 +115,7 @@ mySiloHostBuilder.Configure(options => ## Keep alive -To keep a grain alive, you apply the to the grain implementation. The `KeepAlive` attribute instructs the Orleans runtime to avoid collecting the grain by the idle activation collector. Avoiding collection is useful for grains that are used infrequently but that you want to keep alive to avoid any potential creation overhead. +To keep a grain alive indefinitely, apply the to the grain implementation. The `KeepAlive` attribute instructs the Orleans runtime to avoid collecting the grain by the idle activation collector. Avoiding collection is useful for grains used infrequently but that you want to keep alive to avoid potential creation overhead upon the next invocation. ```csharp public interface IPlayerGrain : IGrainWithGuidKey diff --git a/docs/orleans/host/configuration-guide/adonet-configuration.md b/docs/orleans/host/configuration-guide/adonet-configuration.md index 7965b153f543f..fd59f168e4f91 100644 --- a/docs/orleans/host/configuration-guide/adonet-configuration.md +++ b/docs/orleans/host/configuration-guide/adonet-configuration.md @@ -1,12 +1,13 @@ --- title: ADO.NET database configuration description: Learn about ADO.NET database configurations in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: how-to --- # ADO.NET database configuration -The following sections contain links to SQL scripts to configure your database as well as the corresponding ADO.NET invariant used to configure ADO.NET providers in Orleans. These scripts are intended to be customized if needed for your deployment. Before executing scripts for Clustering, Persistence, or Reminders, one needs to create main tables with the Main scripts. +The following sections contain links to SQL scripts for configuring your database and the corresponding ADO.NET invariant used to configure ADO.NET providers in Orleans. Customize these scripts as needed for your deployment. Before executing scripts for Clustering, Persistence, or Reminders, you need to create the main tables using the Main scripts. ## Main scripts diff --git a/docs/orleans/host/configuration-guide/client-configuration.md b/docs/orleans/host/configuration-guide/client-configuration.md index d9c4c338f3803..088bbbc6c6858 100644 --- a/docs/orleans/host/configuration-guide/client-configuration.md +++ b/docs/orleans/host/configuration-guide/client-configuration.md @@ -1,7 +1,8 @@ --- title: Client configuration description: Learn about client configurations in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: how-to zone_pivot_groups: orleans-version --- @@ -11,7 +12,7 @@ zone_pivot_groups: orleans-version :::zone target="docs" pivot="orleans-7-0" -A client for connecting to a cluster of silos and sending requests to grains is configured programmatically via an and several supplemental option classes. Like silo options, client option classes follow the [Options pattern in .NET](../../../core/extensions/options.md). +Configure a client for connecting to a cluster of silos and sending requests to grains programmatically via an and several supplemental option classes. Like silo options, client option classes follow the [Options pattern in .NET](../../../core/extensions/options.md). :::zone-end @@ -19,20 +20,20 @@ A client for connecting to a cluster of silos and sending requests to grains is :::zone target="docs" pivot="orleans-3-x" -A client for connecting to a cluster of silos and sending requests to grains is configured programmatically via an and several supplemental option classes. Like silo options, client option classes follow the [Options pattern in .NET](../../../core/extensions/options.md). +Configure a client for connecting to a cluster of silos and sending requests to grains programmatically via an and several supplemental option classes. Like silo options, client option classes follow the [Options pattern in .NET](../../../core/extensions/options.md). :::zone-end > [!TIP] > If you just want to start a local silo and a local client for development purposes, see [Local development configuration](local-development-configuration.md). -Add the [Microsoft.Orleans.Clustering.AzureStorage](https://www.nuget.org/packages/Microsoft.Orleans.Clustering.AzureStorage) NuGet package to the client project. +Add the [Microsoft.Orleans.Clustering.AzureStorage](https://www.nuget.org/packages/Microsoft.Orleans.Clustering.AzureStorage) NuGet package to your client project. There are several key aspects of client configuration: -* Orleans clustering information -* Clustering provider -* Application parts +- Orleans clustering information +- Clustering provider +- Application parts Example of a client configuration: @@ -81,7 +82,7 @@ var client = new ClientBuilder() :::zone-end -Let's breakdown the steps used in this sample: +Let's break down the steps used in this sample: ## Orleans clustering information @@ -93,10 +94,10 @@ Let's breakdown the steps used in this sample: }) ``` -Here we set two things: +Here, we set two things: -- the to `"my-first-cluster"`: this is a unique ID for the Orleans cluster. All clients and silo that uses this ID will be able to directly talk to each other. Some will choose to use a different `ClusterId` for each deployments for example. -- the to `"AspNetSampleApp"`: this is a unique ID for your application, that will be used by some provider (for example for persistence providers). This ID should be stable (not change) across deployments. +- The to `"my-first-cluster"`: This is a unique ID for the Orleans cluster. All clients and silos using this ID can directly talk to each other. Some might choose to use a different `ClusterId` for each deployment, for example. +- The to `"AspNetSampleApp"`: This is a unique ID for your application, used by some providers (e.g., persistence providers). This ID should remain stable across deployments. ## Clustering provider @@ -122,7 +123,7 @@ Here we set two things: :::zone-end -The client will discover all gateway available in the cluster using this provider. Several providers are available, here in this sample we use the Azure Table provider. +The client discovers all available gateways in the cluster using this provider. Several providers are available; here, we use the Azure Table provider. For more information, see [Server configuration](server-configuration.md). diff --git a/docs/orleans/host/configuration-guide/configuring-ado-dot-net-providers.md b/docs/orleans/host/configuration-guide/configuring-ado-dot-net-providers.md index b947e59cb6f77..1b05ee68cd991 100644 --- a/docs/orleans/host/configuration-guide/configuring-ado-dot-net-providers.md +++ b/docs/orleans/host/configuration-guide/configuring-ado-dot-net-providers.md @@ -1,14 +1,15 @@ --- title: Configure ADO.NET providers description: Learn how to configure ADO.NET providers in .NET Orleans. -ms.date: 09/10/2024 +ms.date: 05/23/2025 +ms.topic: how-to --- # Configure ADO.NET providers -Any reliable deployment of Orleans requires using persistent storage to keep system state, specifically Orleans cluster membership table and reminders. One of the available options is using a SQL database via the ADO.NET providers. +Any reliable deployment of Orleans requires persistent storage to keep system state, specifically the Orleans cluster membership table and reminders. One available option is using a SQL database via ADO.NET providers. -To use ADO.NET for persistence, clustering, or reminders, one needs to configure the ADO.NET providers as part of the silo configuration, and, in the case of clustering, also as part of the client configurations. +To use ADO.NET for persistence, clustering, or reminders, you need to configure the ADO.NET providers as part of the silo configuration and, for clustering, also as part of the client configurations. The silo configuration code should look like this: @@ -62,11 +63,11 @@ siloHostBuilder.UseAdoNetClustering(options => [!INCLUDE [managed-identities](../../../includes/managed-identities.md)] -Where the `ConnectionString` is set to a valid AdoNet Server connection string. +Set the `ConnectionString` to a valid ADO.NET Server connection string. -To use ADO.NET providers for persistence, reminders, or clustering, there are scripts for creating database artifacts, to which all servers that will be hosting Orleans silos need to have access. The scripts for popular providers can be found in [ADO.NET Configuration](adonet-configuration.md). Lack of access to the target database is a typical mistake we see developers making. +To use ADO.NET providers for persistence, reminders, or clustering, you need scripts to create database artifacts. All servers hosting Orleans silos must have access to the database defined by these scripts. You can find scripts for popular providers in [ADO.NET Database configuration](adonet-configuration.md). Lack of access to the target database is a common mistake developers make. -You also need to install `*.AdoNet` NuGet for features you configure. We split ADO.NET NuGets into per feature NuGets: +You also need to install the `*.AdoNet` NuGet package for the features you configure. We split ADO.NET NuGets into per-feature packages: - `Microsoft.Orleans.Clustering.AdoNet`: for clustering. - `Microsoft.Orleans.Persistence.AdoNet`: for persistence. diff --git a/docs/orleans/host/configuration-guide/configuring-garbage-collection.md b/docs/orleans/host/configuration-guide/configuring-garbage-collection.md index e211d16111fb4..8e971c2b017cb 100644 --- a/docs/orleans/host/configuration-guide/configuring-garbage-collection.md +++ b/docs/orleans/host/configuration-guide/configuring-garbage-collection.md @@ -1,16 +1,17 @@ --- title: Configure .NET garbage collection description: Learn how to configure .NET garbage collection in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: how-to --- # Configure .NET garbage collection -For good performance, it is important to configure .NET garbage collection for the silo process the correct way. The best combination of settings based on the team's findings is to set `gcServer=true` and `gcConcurrent=true`. You can configure these values in the C# project (_.csproj_), or an _app.config_. For more information, see [Flavors of garbage collection](../../../core/runtime-config/garbage-collector.md#flavors-of-garbage-collection). +For good performance, it's important to configure .NET garbage collection correctly for the silo process. Based on the team's findings, the best combination of settings is `gcServer=true` and `gcConcurrent=true`. You can configure these values in your C# project (_.csproj_) or an _app.config_ file. For more information, see [Flavors of garbage collection](../../../core/runtime-config/garbage-collector.md#flavors-of-garbage-collection). ## .NET Core and .NET 5+ -This method is not supported with SDK style projects compiling against the full .NET Framework +This method isn't supported for SDK-style projects compiling against the full .NET Framework. ```xml @@ -21,7 +22,7 @@ This method is not supported with SDK style projects compiling against the full ## .NET Framework -SDK style projects compiling against the full .NET Framework should still use this configuration style, consider an example _app.config_ XML file: +SDK-style projects compiling against the full .NET Framework should still use this configuration style. Consider an example _app.config_ XML file: ``` xml @@ -32,7 +33,7 @@ SDK style projects compiling against the full .NET Framework should still use th ``` -However, this is not as easy to do if a silo runs as part of an Azure Worker Role, which by default is configured to use workstation GC. There's a relevant blog post that discusses how to set the same configuration for an Azure Worker Role, see [Server garbage collection mode in Azure](/archive/blogs/cclayton/server-garbage-collection-mode-in-microsoft-azure). +However, this isn't as easy if a silo runs as part of an Azure Worker Role, which defaults to using workstation GC. A relevant blog post discusses how to set the same configuration for an Azure Worker Role; see [Server garbage collection mode in Azure](/archive/blogs/cclayton/server-garbage-collection-mode-in-microsoft-azure). > [!IMPORTANT] -> Server garbage collection is available only on multiprocessor computers. Therefore, even if you configure the garbage collection either via application _.csproj_ file or via the scripts on the referred blog post, if the silo is running on a (virtual) machine with a single-core, you will not get the benefits of `gcServer=true`. For more information, see [GCSettings.IsServerGC remarks](/dotnet/api/system.runtime.gcsettings.isservergc#remarks). +> Server garbage collection is available only on multiprocessor computers. Therefore, even if you configure garbage collection via the application _.csproj_ file or the scripts in the referred blog post, you won't get the benefits of `gcServer=true` if the silo runs on a (virtual) machine with a single core. For more information, see [GCSettings.IsServerGC remarks](/dotnet/api/system.runtime.gcsettings.isservergc#remarks). diff --git a/docs/orleans/host/configuration-guide/index.md b/docs/orleans/host/configuration-guide/index.md index 268cc1e4e584e..007a10265d58c 100644 --- a/docs/orleans/host/configuration-guide/index.md +++ b/docs/orleans/host/configuration-guide/index.md @@ -1,20 +1,21 @@ --- title: Orleans configuration guide description: Explore a guide on how to configure .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: overview --- # Orleans configuration guide -In this configuration guide, you'll learn the key configuration parameters and how they should be used for most typical usage scenarios. Orleans can be used in a variety of configurations that fit different usage scenarios, such as local single-node deployment for development and testing, clustering of servers, multi-instance Azure worker role, and so on. +In this configuration guide, you learn the key configuration parameters and how to use them for most typical usage scenarios. You can use Orleans in various configurations fitting different scenarios, such as local single-node deployment for development and testing, server clustering, multi-instance Azure worker roles, and so on. -This guide provides instructions for the key configuration parameters that are necessary to make Orleans run in one of the target scenarios. Other configuration parameters primarily help fine-tune Orleans for better performance. +This guide provides instructions for the key configuration parameters necessary to run Orleans in one of the target scenarios. Other configuration parameters primarily help you fine-tune Orleans for better performance. -Silos and clients are configured programmatically via a and respectively. This is possible using several supplemental option classes. Option classes in Orleans follow the [Options pattern in .NET](../../../core/extensions/options.md), and can be loaded via files, environment variables, or any other valid configuration provider. +Configure silos and clients programmatically via and , respectively. You do this using several supplemental option classes. Option classes in Orleans follow the [Options pattern in .NET](../../../core/extensions/options.md) and can be loaded from files, environment variables, or any other valid configuration provider. -If you want to configure a silo and a client for local development, look at the [Local development configuration](local-development-configuration.md) section. The [server configuration](server-configuration.md) and [client configuration](client-configuration.md) sections of the guide cover configuring silos and clients, respectively. +If you want to configure a silo and a client for local development, see the [Local development configuration](local-development-configuration.md) section. The [Server configuration](server-configuration.md) and [Client configuration](client-configuration.md) sections cover configuring silos and clients, respectively. -The section on [typical configurations](typical-configurations.md) provides a summary of a few common configurations. A list of important core options that can be configured can be found on [this section](list-of-options-classes.md). +The section on [Typical configurations](typical-configurations.md) provides a summary of a few common configurations. You can find a list of important core options that you can configure in [List of options classes](list-of-options-classes.md). > [!IMPORTANT] > Make sure you properly configure .NET garbage collection as detailed in [Configure .NET garbage collection](configuring-garbage-collection.md). diff --git a/docs/orleans/host/configuration-guide/list-of-options-classes.md b/docs/orleans/host/configuration-guide/list-of-options-classes.md index d9560412b08a0..1f4003a4ee2f4 100644 --- a/docs/orleans/host/configuration-guide/list-of-options-classes.md +++ b/docs/orleans/host/configuration-guide/list-of-options-classes.md @@ -1,23 +1,24 @@ --- title: List of options classes description: Explore a listing of options classes in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: reference --- # List of options classes -All options classes used to configure Orleans are found in the `Orleans.Configuration` namespace. Many of them have helper methods in the `Orleans.Hosting` namespace. +All options classes used to configure Orleans are found in the `Orleans.Configuration` namespace. Many also have helper methods in the `Orleans.Hosting` namespace. ## Common core options for `IClientBuilder` and `ISiloHostBuilder` -| Option type | Used for | -|------------------------------------------------------------|-----------------------------------------------------------| -| | Setting the `ClusterId` and the `ServiceId` | -| | Setting timeout values for sockets and opened connections | -| ` | Setting the serialization providers | -| | Setting the refresh period of the Type Map (see Heterogeneous silos and Versioning) | +| Option type | Used for | +|--|--| +| | Setting the `ClusterId` and the `ServiceId` | +| | Setting timeout values for sockets and opened connections | +| | Setting the serialization providers | +| | Setting the refresh period of the Type Map (see Heterogeneous silos and Versioning) | -## `IClientBuilder` specific options +## `IClientBuilder`-specific options | Option type | Used for | |------------------------------------------------|---------------------------------------| @@ -26,7 +27,7 @@ All options classes used to configure Orleans are found in the `Orleans.Configur | | Setting the refresh period of the list of available gateways | | | Setting URIs a client will use to connect to cluster | -## `ISiloHostBuilder` specific options +## `ISiloHostBuilder`-specific options | Option type | Used for | |-------------------------------------------------------|---------------------------------| diff --git a/docs/orleans/host/configuration-guide/local-development-configuration.md b/docs/orleans/host/configuration-guide/local-development-configuration.md index 0a62832b55720..0dea3c43d9c36 100644 --- a/docs/orleans/host/configuration-guide/local-development-configuration.md +++ b/docs/orleans/host/configuration-guide/local-development-configuration.md @@ -1,13 +1,14 @@ --- title: Local development configuration description: Learn how to configure .NET Orleans for local development. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: how-to zone_pivot_groups: orleans-version --- # Local development configuration -For a working sample application that targets Orleans 7.0, see [Orleans: Hello World](https://github.com/dotnet/samples/tree/main/orleans/HelloWorld). The sample hosts the client and the silo in .NET console applications that work in different platforms, while the grains and interfaces target .NET Standard 2.0. +For a working sample application targeting Orleans 7.0, see [Orleans: Hello World](https://github.com/dotnet/samples/tree/main/orleans/HelloWorld). The sample hosts the client and silo in .NET console applications that work on different platforms, while the grains and interfaces target .NET Standard 2.0. > [!TIP] > For older versions of Orleans, please see [Orleans sample projects](https://github.com/dotnet/samples/tree/main/orleans). @@ -18,7 +19,7 @@ For a working sample application that targets Orleans 7.0, see [Orleans: Hello W :::zone target="docs" pivot="orleans-7-0" -It's recommended to use the [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting) NuGet package to configure and run the silo. Also, when developing an Orleans silo you need the [Microsoft.Orleans.Server](https://www.nuget.org/packages/Microsoft.Orleans.Server) NuGet package. For local Orleans silo development, you configure localhost clustering, which is configured to use the loopback address. To use localhost clustering, call the extension method. Consider this example _Program.cs_ file of the silo host: +We recommend using the [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting) NuGet package to configure and run the silo. Also, when developing an Orleans silo, you need the [Microsoft.Orleans.Server](https://www.nuget.org/packages/Microsoft.Orleans.Server) NuGet package. For local Orleans silo development, configure localhost clustering, which uses the loopback address. To use localhost clustering, call the extension method. Consider this example _Program.cs_ file for the silo host: ```csharp using Microsoft.Extensions.Hosting; @@ -31,7 +32,7 @@ await Host.CreateDefaultBuilder(args) .RunConsoleAsync(); ``` -The preceding code: +The preceding code does the following: - Creates a default host builder. - Calls the `UseOrleans` extension method to configure the silo. @@ -44,27 +45,21 @@ The preceding code: :::zone target="docs" pivot="orleans-3-x" -For local development, refer to the below example of how to configure a silo for that case. It configures and starts a silo listening on the `loopback` address, `11111` and `30000` as silo and gateway ports respectively. +For local development, refer to the example below showing how to configure a silo for this case. It configures and starts a silo listening on the `loopback` address, using `11111` and `30000` as the silo and gateway ports, respectively. -Add the `Microsoft.Orleans.Server` NuGet meta-package to the project. +Add the `Microsoft.Orleans.Server` NuGet meta-package to your project. ```dotnetcli dotnet add package Microsoft.Orleans.Server ``` -Or, in .NET 10+: - -```dotnetcli -dotnet package add Microsoft.Orleans.Server -``` - You need to configure via `Configure` method, specify that you want `LocalhostClustering` as your clustering choice with this silo being the primary, and then configure silo endpoints. -The call explicitly adds the assembly with grain classes to the application setup. It also adds any referenced assembly due to the extension. After these steps are completed, the silo host gets built and the silo gets started. +The call explicitly adds the assembly containing grain classes to the application setup. It also adds any referenced assembly due to the extension. After completing these steps, build the silo host and start the silo. -You can create an empty console application project targeting .NET Framework 4.6.1 or higher for hosting a silo, and a .NET console application. +You can create an empty console application project targeting .NET Framework 4.6.1 or higher for hosting a silo. -Here's an example of how a local silo can be started: +Here's an example of how you can start a local silo: ```csharp try @@ -112,7 +107,7 @@ static async Task BuildAndStartSiloAsync() :::zone target="docs" pivot="orleans-7-0" -It's recommended to use the [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting) NuGet package to configure and run clients (in addition to the silo). You also need the [Microsoft.Orleans.Client](https://www.nuget.org/packages/Microsoft.Orleans.Client) NuGet package. To use localhost clustering on the consuming client, call the extension method. Consider this example _Program.cs_ file of the client host: +We recommend using the [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting) NuGet package to configure and run clients (in addition to the silo). You also need the [Microsoft.Orleans.Client](https://www.nuget.org/packages/Microsoft.Orleans.Client) NuGet package. To use localhost clustering on the consuming client, call the extension method. Consider this example _Program.cs_ file for the client host: ```csharp using Microsoft.Extensions.Hosting; @@ -128,7 +123,7 @@ using IHost host = Host.CreateDefaultBuilder(args) await host.StartAsync(); ``` -The preceding code: +The preceding code does the following: - Creates a default host builder. - Calls the `UseOrleansClient` extension method to configure the client. @@ -142,21 +137,21 @@ The preceding code: :::zone target="docs" pivot="orleans-3-x" -For local development, refer to the below example of how to configure a client for that case. It configures a client that would connect to a `loopback` silo. +For local development, refer to the example below showing how to configure a client for this case. It configures a client that connects to a `loopback` silo. -Add the `Microsoft.Orleans.Client` NuGet meta-package to the project. After you get comfortable with the API, you can pick and choose which exact packages included in `Microsoft.Orleans.Client` you actually need and reference them instead. +Add the `Microsoft.Orleans.Client` NuGet meta-package to your project. After you become comfortable with the API, you can pick and choose the exact packages included in `Microsoft.Orleans.Client` that you need and reference them instead. ```powershell Install-Package Microsoft.Orleans.Client ``` -You need to configure with a cluster ID that matches the one you specified for the local silo and specify static clustering as your clustering choice pointing it to the gateway port of the silo +Configure with a cluster ID matching the one specified for the local silo. Specify static clustering as your clustering choice, pointing it to the silo's gateway port. -`ConfigureApplicationParts` call explicitly adds the assembly with grain interfaces to the application setup. +The `ConfigureApplicationParts` call explicitly adds the assembly containing grain interfaces to the application setup. -After these steps are completed, we can build the client and `Connect()` method on it to connect to the cluster. +After completing these steps, build the client and call its `Connect()` method to connect to the cluster. -You can create an empty console application project targeting .NET Framework 4.6.1 or higher for running a client or reuse the console application project you created for hosting a silo. +You can create an empty console application project targeting .NET Framework 4.6.1 or higher for running a client, or reuse the console application project created for hosting the silo. Here's an example of how a client can connect to a local silo: diff --git a/docs/orleans/host/configuration-guide/serialization-configuration.md b/docs/orleans/host/configuration-guide/serialization-configuration.md index 4879cce52fb3e..9ae23a9e772a8 100644 --- a/docs/orleans/host/configuration-guide/serialization-configuration.md +++ b/docs/orleans/host/configuration-guide/serialization-configuration.md @@ -1,7 +1,8 @@ --- title: Serialization configuration in Orleans description: Learn how to configure serialization in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: how-to uid: orleans-serialization-configuration zone_pivot_groups: orleans-version --- @@ -12,11 +13,11 @@ zone_pivot_groups: orleans-version :::zone target="docs" pivot="orleans-7-0" -The configuration of serialization in Orleans is a crucial part of the overall system design. While Orleans provides reasonable defaults, you can configure serialization to suit your apps' needs. For sending data between hosts, supports delegating to other serializers, such as [Newtonsoft.Json](https://www.nuget.org/packages/Newtonsoft.Json) and [System.Text.Json](https://www.nuget.org/packages/System.Text.Json). You can add support for other serializers by following the pattern set by those implementations. For grain storage it's best to use to configure a custom serializer. +Serialization configuration in Orleans is a crucial part of the overall system design. While Orleans provides reasonable defaults, you can configure serialization to suit your app's needs. For sending data between hosts, supports delegating to other serializers, such as [Newtonsoft.Json](https://www.nuget.org/packages/Newtonsoft.Json) and [System.Text.Json](https://www.nuget.org/packages/System.Text.Json). You can add support for other serializers by following the pattern set by those implementations. For grain storage, it's best to use to configure a custom serializer. ## Configure Orleans to use `Newtonsoft.Json` -To configure Orleans to serialize certain types using `Newtonsoft.Json`, you must first reference the [Microsoft.Orleans.Serialization.NewtonsoftJson](https://nuget.org/packages/Microsoft.Orleans.Serialization.NewtonsoftJson) NuGet package. Then, configure the serializer, specifying which types it will be responsible for. In the following example, we will specify that the `Newtonsoft.Json` serializer will be responsible for all types in the `Example.Namespace` namespace. +To configure Orleans to serialize certain types using `Newtonsoft.Json`, first reference the [Microsoft.Orleans.Serialization.NewtonsoftJson](https://nuget.org/packages/Microsoft.Orleans.Serialization.NewtonsoftJson) NuGet package. Then, configure the serializer, specifying which types it will be responsible for. In the following example, we specify that the `Newtonsoft.Json` serializer is responsible for all types in the `Example.Namespace` namespace. ``` csharp siloBuilder.Services.AddSerializer(serializerBuilder => @@ -26,13 +27,13 @@ siloBuilder.Services.AddSerializer(serializerBuilder => }); ``` -In the preceding example, the call to adds support for serializing and deserializing values using `Newtonsoft.Json.JsonSerializer`. Similar configuration must be performed on all clients that need to handle those types. +In the preceding example, the call to adds support for serializing and deserializing values using `Newtonsoft.Json.JsonSerializer`. You must perform similar configuration on all clients that need to handle those types. -For types that are marked with ), Orleans will prefer the generated serializer over the `Newtonsoft.Json` serializer. +For types marked with , Orleans prefers the generated serializer over the `Newtonsoft.Json` serializer. ## Configure Orleans to use `System.Text.Json` -Alternatively, to configure Orleans to use `System.Text.Json` to serialize your types, you reference the [Microsoft.Orleans.Serialization.SystemTextJson](https://nuget.org/packages/Microsoft.Orleans.Serialization.SystemTextJson) NuGet package. Then, configure the serializer, specifying which types it will be responsible for. In the following example, we will specify that the `System.Text.Json` serializer will be responsible for all types in the `Example.Namespace` namespace. +Alternatively, to configure Orleans to use `System.Text.Json` to serialize your types, reference the [Microsoft.Orleans.Serialization.SystemTextJson](https://nuget.org/packages/Microsoft.Orleans.Serialization.SystemTextJson) NuGet package. Then, configure the serializer, specifying which types it will be responsible for. In the following example, we specify that the `System.Text.Json` serializer is responsible for all types in the `Example.Namespace` namespace. - Install the [Microsoft.Orleans.Serialization.SystemTextJson](https://nuget.org/packages/Microsoft.Orleans.Serialization.SystemTextJson) NuGet package. - Configure the serializer using the method. @@ -55,9 +56,9 @@ siloBuilder.Services.AddSerializer(serializerBuilder => ## External serializer providers -It's important to ensure that serialization configuration is identical on all clients and silos. If configurations are inconsistent, serialization errors may occur. +Ensure serialization configuration is identical on all clients and silos. Inconsistent configurations can lead to serialization errors. -Serialization providers that implement `IExternalSerializer` can be specified using the property of and in code: +Specify serialization providers implementing `IExternalSerializer` using the property of and in code: ```csharp // Client configuration @@ -71,7 +72,7 @@ globalConfiguration.SerializationProviders.Add( typeof(FantasticSerializer).GetTypeInfo()); ``` -Alternatively, they can be specified in XML configuration under the `` property of ``: +Alternatively, specify them in XML configuration under the `` property of ``: ```xml @@ -81,7 +82,7 @@ Alternatively, they can be specified in XML configuration under the ` ``` -In both cases, multiple providers can be configured. The collection is ordered, meaning that if a provider which can serialize types `A` and `B` is specified before a provider which can only serialize type `B`, then the latter provider will not be used. +In both cases, you can configure multiple providers. The collection is ordered. This means if a provider capable of serializing types `A` and `B` is specified before a provider that can only serialize type `B`, the latter provider won't be used for type `B`. :::zone-end diff --git a/docs/orleans/host/configuration-guide/serialization-customization.md b/docs/orleans/host/configuration-guide/serialization-customization.md index 4c22a4d683606..727aa733fb769 100644 --- a/docs/orleans/host/configuration-guide/serialization-customization.md +++ b/docs/orleans/host/configuration-guide/serialization-customization.md @@ -101,9 +101,9 @@ Additional considerations would be to expose an overload that accepts custom ser Orleans supports integration with third-party serializers using a provider model. This requires an implementation of the type described in the custom serialization section of this article. Integrations for some common serializers are maintained alongside Orleans, for example: -* [Protocol Buffers](https://developers.google.com/protocol-buffers/): from the [Microsoft.Orleans.OrleansGoogleUtils](https://www.nuget.org/packages/Microsoft.Orleans.OrleansGoogleUtils/) NuGet package. -* [Bond](https://github.com/microsoft/bond/): from the [Microsoft.Orleans.Serialization.Bond](https://www.nuget.org/packages/Microsoft.Orleans.Serialization.Bond/) NuGet package. -* [Newtonsoft.Json](https://www.newtonsoft.com/json): from the core Orleans library. +- [Protocol Buffers](https://developers.google.com/protocol-buffers/): from the [Microsoft.Orleans.OrleansGoogleUtils](https://www.nuget.org/packages/Microsoft.Orleans.OrleansGoogleUtils/) NuGet package. +- [Bond](https://github.com/microsoft/bond/): from the [Microsoft.Orleans.Serialization.Bond](https://www.nuget.org/packages/Microsoft.Orleans.Serialization.Bond/) NuGet package. +- [Newtonsoft.Json](https://www.newtonsoft.com/json): from the core Orleans library. Custom implementation of `IExternalSerializer` is described in the following section. @@ -123,9 +123,9 @@ Each of these serialization methods is detailed in the following sections. Orleans serialization happens in three stages: -* Objects are immediately deep copied to ensure isolation. -* Before being put on the wire, objects are serialized to a message byte stream. -* When delivered to the target activation, objects are recreated (deserialized) from the received byte stream. +- Objects are immediately deep copied to ensure isolation. +- Before being put on the wire, objects are serialized to a message byte stream. +- When delivered to the target activation, objects are recreated (deserialized) from the received byte stream. Data types that may be sent in messages—that is, types that may be passed as method arguments or return values—must have associated routines that perform these three steps. We refer to these routines collectively as the serializers for a data type. diff --git a/docs/orleans/host/configuration-guide/serialization-immutability.md b/docs/orleans/host/configuration-guide/serialization-immutability.md index 90748b45bfed8..c8943cfce5c79 100644 --- a/docs/orleans/host/configuration-guide/serialization-immutability.md +++ b/docs/orleans/host/configuration-guide/serialization-immutability.md @@ -1,33 +1,33 @@ --- title: Serialization of immutable types in Orleans description: Learn how .NET Orleans handles type immutability in the context of serialization. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Serialization of immutable types in Orleans -Orleans has a feature that can be used to avoid some of the overhead associated with serializing messages containing immutable types. This section describes the feature and its application, starting with context on where it is relevant. +Orleans has a feature you can use to avoid some overhead associated with serializing messages containing immutable types. This section describes the feature and its application, starting with the context where it's relevant. ## Serialization in Orleans -When a grain method is invoked, the Orleans runtime makes a deep copy of the method arguments and forms the request out of the copies. This protects against the calling code modifying the argument objects before the data is passed to the called grain. +When you invoke a grain method, the Orleans runtime makes a deep copy of the method arguments and forms the request from these copies. This protects against the calling code modifying the argument objects before the data passes to the called grain. -If the called grain is on a different silo, then the copies are eventually serialized into a byte stream and sent over the network to the target silo, where they are deserialized back into objects. If the called grain is on the same silo, then the copies are handed directly to the called method. +If the called grain is on a different silo, the copies are eventually serialized into a byte stream and sent over the network to the target silo, where they are deserialized back into objects. If the called grain is on the same silo, the copies are handed directly to the called method. Return values are handled the same way: first copied, then possibly serialized and deserialized. -Note that all 3 processes, copying, serializing, and deserializing, respect object identity. In other words, if you pass a list that has the same object in it twice, on the receiving side you'll get a list with the same object in it twice, rather than with two objects with the same values in them. +Note that all three processes—copying, serializing, and deserializing—respect object identity. In other words, if you pass a list containing the same object twice, the receiving side gets a list with the same object twice, rather than two objects with the same values. ## Optimize copying -In many cases, deep copying is unnecessary. For instance, a possible scenario is a web front-end that receives a byte array from its client and passes that request, including the byte array, onto a grain for processing. The front-end process doesn't do anything with the array once it has passed it on to the grain; in particular, it doesn't reuse the array to receive a future request. Inside the grain, the byte array is parsed to fetch the input data, but not modified. The grain returns another byte array that it has created to get passed back to the web client; it discards the array as soon as it returns it. The web front-end passes the result byte array back to its client, without modification. +In many cases, deep copying is unnecessary. For instance, consider a scenario where a web front-end receives a byte array from its client and passes that request, including the byte array, to a grain for processing. The front-end process does nothing with the array after passing it to the grain; specifically, it doesn't reuse the array for future requests. Inside the grain, the byte array is parsed to fetch input data but isn't modified. The grain returns another byte array it created back to the web client and discards the array immediately after returning it. The web front-end passes the result byte array back to its client without modification. -In such a scenario, there is no need to copy either the request or response byte arrays. Unfortunately, the Orleans runtime can't figure this out by itself, since it can't tell whether or not the arrays are modified later on by the web front-end or by the grain. In the best of all possible worlds, we'd have some sort of .NET mechanism for indicating that a value is no longer modified; lacking that, we've added Orleans-specific mechanisms for this: the wrapper class and the . +In such a scenario, there's no need to copy either the request or response byte arrays. Unfortunately, the Orleans runtime can't figure this out automatically, as it can't determine whether the web front-end or the grain modifies the arrays later. Ideally, a .NET mechanism would indicate that a value is no longer modified. Lacking that, we've added Orleans-specific mechanisms: the wrapper class and the . -### Use the `[Immutable]` attribute to make a type, parameter, property, or field as immutable +### Use the `[Immutable]` attribute to mark a type, parameter, property, or field as immutable -For user-defined types, the can be added to the type. This instructs Orleans' serializer to avoid copying instances of this type. -The following code snippet demonstrates using `[Immutable]` to denote an immutable type. This type will not be copied during transmission. +For user-defined types, you can add the to the type. This instructs the Orleans serializer to avoid copying instances of this type. The following code snippet demonstrates using `[Immutable]` to denote an immutable type. This type won't be copied during transmission. ```csharp [Immutable] @@ -42,7 +42,7 @@ public class MyImmutableType } ``` -Sometimes, you may not have control over the object, for example, it may be a `List` that you are sending between grains. Other times, perhaps parts of your objects are immutable and other parts are not. For these cases, Orleans supports additional options. +Sometimes, you might not control the object; for example, it might be a `List` you're sending between grains. Other times, parts of your objects might be immutable while others aren't. For these cases, Orleans supports additional options. 1. Method signatures can include on a per-parameter basis: @@ -54,7 +54,7 @@ Sometimes, you may not have control over the object, for example, it may be a `L } ``` -1. Individual properties and fields can be marked as to prevent copies being made when instances of the containing type are copied. +1. Mark individual properties and fields as to prevent copies when instances of the containing type are copied. ```csharp [GenerateSerializer] @@ -70,9 +70,9 @@ Sometimes, you may not have control over the object, for example, it may be a `L ### Use `Immutable` -The wrapper class is used to indicate that a value may be considered immutable; that is, the underlying value will not be modified, so no copying is required for safe sharing. Note that using `Immutable` implies that neither the provider of the value nor the recipient of the value will modify it in the future; it is not a one-sided commitment, but rather a mutual dual-side commitment. +Use the wrapper class to indicate a value can be considered immutable; that is, the underlying value won't be modified, so no copying is required for safe sharing. Note that using `Immutable` implies neither the provider nor the recipient of the value will modify it in the future. It's a mutual, dual-sided commitment, not a one-sided one. -To use `Immutable` in your grain interface, instead of passing `T`, pass `Immutable`. For instance, in the above-described scenario, the grain method was: +To use `Immutable` in your grain interface, pass `Immutable` instead of `T`. For instance, in the scenario described above, the grain method was: ```csharp Task ProcessRequest(byte[] request); @@ -84,13 +84,13 @@ Which would then become: Task> ProcessRequest(Immutable request); ``` -To create an `Immutable`, simply use the constructor: +To create an `Immutable`, simply use its constructor: ```csharp Immutable immutable = new(buffer); ``` -To get the values inside the immutable, use the `.Value` property: +To get the value inside the immutable wrapper, use the `.Value` property: ```csharp byte[] buffer = immutable.Value; @@ -98,6 +98,6 @@ byte[] buffer = immutable.Value; ## Immutability in Orleans -For Orleans' purposes, immutability is a rather strict statement: the contents of the data item will not be modified in any way that could change the item's semantic meaning, or that would interfere with another thread simultaneously accessing the item. The safest way to ensure this is to simply not modify the item at all: bitwise immutability, rather than logical immutability. +For Orleans' purposes, immutability is a strict statement: the contents of the data item won't be modified in any way that could change the item's semantic meaning or interfere with another thread simultaneously accessing it. The safest way to ensure this is simply not to modify the item at all: use bitwise immutability rather than logical immutability. -In some cases, it is safe to relax this to logical immutability, but care must be taken to ensure that the mutating code is properly thread-safe. Because dealing with multithreading is complex, and uncommon in an Orleans context, we strongly recommend against this approach and recommend sticking to bitwise immutability. +In some cases, it's safe to relax this to logical immutability, but you must take care to ensure the mutating code is properly thread-safe. Because dealing with multithreading is complex and uncommon in an Orleans context, we strongly recommend against this approach and advise sticking to bitwise immutability. diff --git a/docs/orleans/host/configuration-guide/serialization.md b/docs/orleans/host/configuration-guide/serialization.md index 61b0e5606479f..92c47b6c7797e 100644 --- a/docs/orleans/host/configuration-guide/serialization.md +++ b/docs/orleans/host/configuration-guide/serialization.md @@ -1,7 +1,8 @@ --- title: Serialization in Orleans description: Learn about serialization and custom serializers in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: overview uid: orleans-serialization zone_pivot_groups: orleans-version --- @@ -14,31 +15,31 @@ zone_pivot_groups: orleans-version There are broadly two kinds of serialization used in Orleans: -* **Grain call serialization** - used to serialize objects passed to and from grains. -* **Grain storage serialization** - used to serialize objects to and from storage systems. +- **Grain call serialization**: Used to serialize objects passed to and from grains. +- **Grain storage serialization**: Used to serialize objects to and from storage systems. -The majority of this article is dedicated to grain call serialization via the serialization framework included in Orleans. The [Grain storage serializers](#grain-storage-serializers) section discusses the grain storage serialization. +Most of this article focuses on grain call serialization via the serialization framework included in Orleans. The [Grain storage serializers](#grain-storage-serializers) section discusses grain storage serialization. ## Use Orleans serialization -Orleans includes an advanced and extensible serialization framework which can be referred to as **Orleans.Serialization**. The serialization framework included in Orleans is designed to meet the following goals: +Orleans includes an advanced and extensible serialization framework referred to as **Orleans.Serialization**. The serialization framework included in Orleans is designed to meet the following goals: -* **High-performance** - The serializer is designed and optimized for performance. More details are available in [this presentation](https://www.youtube.com/watch?v=kgRag4E6b4c). -* **High-fidelity** - The serializer faithfully represents the majority of .NET's type system, including support for generics, polymorphism, inheritance hierarchies, object identity, and cyclic graphs. Pointers are not supported, since they are not portable across processes. -* **Flexibility** - The serializer can be customized to support third-party libraries by creating [*surrogates*](#surrogates-for-serializing-foreign-types) or delegating to external serialization libraries such as **System.Text.Json**, **Newtonsoft.Json**, and **Google.Protobuf**. -* **Version-tolerance** - The serializer allows application types to evolve over time, supporting: - * Adding and removing members - * Sub-classing - * Numeric widening and narrowing (e.g: `int` to/from `long`, `float` to/from `double`) - * Renaming types +- **High-performance**: The serializer is designed and optimized for performance. More details are available in [this presentation](https://www.youtube.com/watch?v=kgRag4E6b4c). +- **High-fidelity**: The serializer faithfully represents most of .NET's type system, including support for generics, polymorphism, inheritance hierarchies, object identity, and cyclic graphs. Pointers aren't supported since they aren't portable across processes. +- **Flexibility**: You can customize the serializer to support third-party libraries by creating [*surrogates*](#surrogates-for-serializing-foreign-types) or delegating to external serialization libraries such as **System.Text.Json**, **Newtonsoft.Json**, and **Google.Protobuf**. +- **Version-tolerance**: The serializer allows application types to evolve over time, supporting: + - Adding and removing members + - Subclassing + - Numeric widening and narrowing (e.g., `int` to/from `long`, `float` to/from `double`) + - Renaming types -High-fidelity representation of types is fairly uncommon for serializers, so some points warrant further elaboration: +High-fidelity representation of types is fairly uncommon for serializers, so some points warrant further explanation: -1. **Dynamic types and arbitrary polymorphism**: Orleans doesn't enforce restrictions on the types that can be passed in grain calls and maintain the dynamic nature of the actual data type. That means, for example, that if the method in the grain interfaces is declared to accept but at runtime, the sender passes , the receiver will indeed get `SortedDictionary` (although the "static contract"/grain interface did not specify this behavior). +1. **Dynamic types and arbitrary polymorphism**: Orleans doesn't enforce restrictions on the types passed in grain calls and maintains the dynamic nature of the actual data type. This means, for example, if a method in a grain interface is declared to accept , but at runtime the sender passes a , the receiver indeed gets a `SortedDictionary` (even though the "static contract"/grain interface didn't specify this behavior). -1. **Maintaining object identity**: If the same object is passed multiple types in the arguments of a grain call or is indirectly pointed more than once from the arguments, Orleans will serialize it only once. On the receiver side, Orleans will restore all references correctly so that two pointers to the same object still point to the same object after deserialization as well. Object identity is important to preserve in scenarios like the following. Imagine grain A is sending a dictionary with 100 entries to grain B, and 10 of the keys in the dictionary point to the same object, `obj`, on A's side. Without preserving object identity, B would receive a dictionary of 100 entries with those 10 keys pointing to 10 different clones of `obj`. With object identity-preserved, the dictionary on B's side looks exactly like on A's side with those 10 keys pointing to a single object `obj`. Note that because the default string hash code implementations in .NET are randomized per-process, ordering of values in dictionaries and hash sets (for example) may not be preserved. +1. **Maintaining object identity**: If the same object is passed multiple times in the arguments of a grain call or is indirectly pointed to more than once from the arguments, Orleans serializes it only once. On the receiver side, Orleans restores all references correctly so that two pointers to the same object still point to the same object after deserialization. Preserving object identity is important in scenarios like the following: Imagine grain A sends a dictionary with 100 entries to grain B, and 10 keys in the dictionary point to the same object, `obj`, on A's side. Without preserving object identity, B would receive a dictionary of 100 entries with those 10 keys pointing to 10 different clones of `obj`. With object identity preserved, the dictionary on B's side looks exactly like on A's side, with those 10 keys pointing to a single object `obj`. Note that because default string hash code implementations in .NET are randomized per process, the ordering of values in dictionaries and hash sets (for example) might not be preserved. -To support version tolerance, the serializer requires developers to be explicit about which types and members are serialized. We have tried to make this as pain-free as possible. You must mark all serializable types with to instruct Orleans to generate serializer code for your type. Once you have done this, you can use the included code-fix to add the required to the serializable members on your types, as demonstrated here: +To support version tolerance, the serializer requires you to be explicit about which types and members are serialized. We've tried to make this as painless as possible. Mark all serializable types with to instruct Orleans to generate serializer code for your type. Once you've done this, you can use the included code fix to add the required to the serializable members on your types, as demonstrated here: :::image type="content" source="media/generate-serializer-code-fix.gif" alt-text="An animated image of the available code fix being suggested and applied on the GenerateSerializerAttribute when the containing type doesn't contain IdAttribute's on its members." lightbox="media/generate-serializer-code-fix.gif"::: @@ -53,7 +54,7 @@ public class Employee } ``` -Orleans supports inheritance and will serialize the individual layers in the hierarchy separately, allowing them to have distinct member ids. +Orleans supports inheritance and serializes the individual layers in the hierarchy separately, allowing them to have distinct member IDs. ```csharp [GenerateSerializer] @@ -71,7 +72,7 @@ public class Book : Publication } ``` -In the preceding code, note that both `Publication` and `Book` have members with `[Id(0)]` even though `Book` derives from `Publication`. This is the recommended practice in Orleans because members identifiers are scoped to the inheritance level, not the type as a whole. Members can be added and removed from `Publication` and `Book` independently, but a new base class cannot be inserted into the hierarchy once the application has been deployed without special consideration. +In the preceding code, note that both `Publication` and `Book` have members with `[Id(0)]`, even though `Book` derives from `Publication`. This is the recommended practice in Orleans because member identifiers are scoped to the inheritance level, not the type as a whole. You can add and remove members from `Publication` and `Book` independently, but you cannot insert a new base class into the hierarchy once the application is deployed without special consideration. Orleans also supports serializing types with `internal`, `private`, and `readonly` members, such as in this example type: @@ -95,11 +96,11 @@ public struct MyCustomStruct } ``` -By default, Orleans will serialize your type by encoding its full name. You can override this by adding an . Doing so will result in your type being serialized using a name that is resilient to renaming the underlying class or moving it between assemblies. Type aliases are globally scoped, and you cannot have two aliases with the same value in an application. For generic types, the alias value must include the number of generic parameters preceded by a backtick, for example, `MyGenericType` could have the alias [Alias("mytype\`2")]. +By default, Orleans serializes your type by encoding its full name. You can override this by adding an . Doing so results in your type being serialized using a name resilient to renaming the underlying class or moving it between assemblies. Type aliases are globally scoped, and you cannot have two aliases with the same value in an application. For generic types, the alias value must include the number of generic parameters preceded by a backtick; for example, `MyGenericType` could have the alias [Alias("mytype\`2")]. ## Serializing `record` types -Members defined in a record's primary constructor have implicit ids by default. In other words, Orleans supports serializing `record` types. This means that you cannot change the parameter order for an already deployed type, since that breaks compatibility with previous versions of your application (in the case of a rolling upgrade) and with serialized instances of that type in storage and streams. Members defined in the body of a record type don't share identities with the primary constructor parameters. +Members defined in a record's primary constructor have implicit IDs by default. In other words, Orleans supports serializing `record` types. This means you cannot change the parameter order for an already deployed type, as that breaks compatibility with previous versions of your application (in a rolling upgrade scenario) and with serialized instances of that type in storage and streams. Members defined in the body of a record type don't share identities with the primary constructor parameters. ```csharp [GenerateSerializer] @@ -111,11 +112,11 @@ public record MyRecord(string A, string B) } ``` -If you don't want the primary constructor parameters to be automatically included as Serializable fields, you can use `[GenerateSerializer(IncludePrimaryConstructorParameters = false)]`. +If you don't want the primary constructor parameters automatically included as serializable fields, use `[GenerateSerializer(IncludePrimaryConstructorParameters = false)]`. ## Surrogates for serializing foreign types -Sometimes you may need to pass types between grains which you don't have full control over. In these cases, it may be impractical to convert to and from some custom-defined type in your application code manually. Orleans offers a solution for these situations in the form of surrogate types. Surrogates are serialized in place of their target type and have functionality to convert to and from the target type. Consider the following example of a foreign type and a corresponding surrogate and converter: +Sometimes, you might need to pass types between grains over which you don't have full control. In these cases, manually converting to and from a custom-defined type in your application code might be impractical. Orleans offers a solution for these situations: surrogate types. Surrogates are serialized in place of their target type and have functionality to convert to and from the target type. Consider the following example of a foreign type and a corresponding surrogate and converter: ``` csharp // This is the foreign type, which you do not have control over. @@ -170,11 +171,11 @@ public sealed class MyForeignLibraryValueTypeSurrogateConverter : In the preceding code: -- The `MyForeignLibraryValueType` is a type outside of your control, defined in a consuming library. -- The `MyForeignLibraryValueTypeSurrogate` is a surrogate type that maps to `MyForeignLibraryValueType`. -- The specifies that the `MyForeignLibraryValueTypeSurrogateConverter` acts as a converter to map to and from the two types. The class is an implementation of the interface. +- `MyForeignLibraryValueType` is a type outside your control, defined in a consuming library. +- `MyForeignLibraryValueTypeSurrogate` is a surrogate type mapping to `MyForeignLibraryValueType`. +- specifies that `MyForeignLibraryValueTypeSurrogateConverter` acts as a converter to map between the two types. The class implements the interface. -Orleans supports serialization of types in type hierarchies (types which derive from other types). In the event that a foreign type might appear in a type hierarchy (for example as the base class for one of your own types), you must additionally implement the interface. Consider the following example: +Orleans supports serialization of types in type hierarchies (types deriving from other types). If a foreign type might appear in a type hierarchy (e.g., as the base class for one of your own types), you must additionally implement the interface. Consider the following example: ``` csharp // The foreign type is not sealed, allowing other types to inherit from it. @@ -256,38 +257,38 @@ public sealed class DerivedFromMyForeignLibraryType : MyForeignLibraryType ## Versioning rules -Version-tolerance is supported provided the developer follows a set of rules when modifying types. If the developer is familiar with systems such as Google Protocol Buffers (Protobuf), then these rules will be familiar. +Version tolerance is supported provided you follow a set of rules when modifying types. If you're familiar with systems like Google Protocol Buffers (Protobuf), these rules will be familiar. ### Compound types (`class` & `struct`) -* Inheritance is supported, but modifying the inheritance hierarchy of an object is not supported. The base class of a class cannot be added, changed to another class, or removed. -* With the exception of some numeric types, described in the *Numerics* section below, field types cannot be changed. -* Fields can be added or removed at any point in an inheritance hierarchy. -* Field ids cannot be changed. -* Field ids must be unique for each level in a type hierarchy, but can be reused between base-classes and sub-classes. For example, `Base` class can declare a field with id `0` and a different field can be declared by `Sub : Base` with the same id, `0`. +- Inheritance is supported, but modifying the inheritance hierarchy of an object isn't supported. You cannot add, change, or remove the base class of a class. +- With the exception of some numeric types described in the *Numerics* section below, you cannot change field types. +- You can add or remove fields at any point in an inheritance hierarchy. +- You cannot change field IDs. +- Field IDs must be unique for each level in a type hierarchy but can be reused between base classes and subclasses. For example, a `Base` class can declare a field with ID `0`, and a `Sub : Base` class can declare a different field with the same ID, `0`. ### Numerics -* The *signedness* of a numeric field cannot be changed. - * Conversions between `int` & `uint` are invalid. -* The *width* of a numeric field can be changed. - * Eg: conversions from `int` to `long` or `ulong` to `ushort` are supported. - * Conversions which narrow the width will throw if the runtime value of a field would cause an overflow. - * Conversion from `ulong` to `ushort` are only supported if the value at runtime is less than `ushort.MaxValue`. - * Conversions from `double` to `float` are only supported if the runtime value is between `float.MinValue` and `float.MaxValue`. - * Similarly for `decimal`, which has a narrower range than both `double` and `float`. +- You cannot change the *signedness* of a numeric field. + - Conversions between `int` & `uint` are invalid. +- You can change the *width* of a numeric field. + - E.g., conversions from `int` to `long` or `ulong` to `ushort` are supported. + - Conversions narrowing the width throw an exception if the field's runtime value causes an overflow. + - Conversion from `ulong` to `ushort` is only supported if the runtime value is less than `ushort.MaxValue`. + - Conversions from `double` to `float` are only supported if the runtime value is between `float.MinValue` and `float.MaxValue`. + - Similarly for `decimal`, which has a narrower range than both `double` and `float`. ## Copiers -Orleans promotes safety by default. This includes safety from some classes of concurrency bugs. In particular, Orleans will immediately copy objects passed in grain calls by default. This copying is facilitated by Orleans.Serialization and when is applied to a type, Orleans will also generate copiers for that type. Orleans will avoid copying types or individual members which are marked using the . For more details, see [Serialization of immutable types in Orleans](./serialization-immutability.md). +Orleans promotes safety by default, including safety from some classes of concurrency bugs. In particular, Orleans immediately copies objects passed in grain calls by default. Orleans.Serialization facilitates this copying. When you apply to a type, Orleans also generates copiers for that type. Orleans avoids copying types or individual members marked with . For more details, see [Serialization of immutable types in Orleans](./serialization-immutability.md). ## Serialization best practices - ✅ **Do** give your types aliases using the `[Alias("my-type")]` attribute. Types with aliases can be renamed without breaking compatibility. -- ❌ **Do not** change a `record` to a regular `class` or vice-versa. Records and classes are not represented in an identical fashion since records have primary constructor members in addition to regular members and therefore the two are not interchangeable. +- ❌ **Do not** change a `record` to a regular `class` or vice-versa. Records and classes aren't represented identically since records have primary constructor members in addition to regular members; therefore, the two aren't interchangeable. - ❌ **Do not** add new types to an existing type hierarchy for a serializable type. You must not add a new base class to an existing type. You can safely add a new subclass to an existing type. - ✅ **Do** replace usages of with and corresponding declarations. -- ✅ **Do** start all member ids at zero for each type. Ids in a subclass and its base class can safely overlap. Both properties in the following example have ids equal to `0`. +- ✅ **Do** start all member IDs at zero for each type. IDs in a subclass and its base class can safely overlap. Both properties in the following example have IDs equal to `0`. ```csharp [GenerateSerializer] @@ -306,19 +307,19 @@ Orleans promotes safety by default. This includes safety from some classes of co ``` - ✅ **Do** widen numeric member types as needed. You can widen `sbyte` to `short` to `int` to `long`. - - You can narrow numeric member types but it will result in a runtime exception if observed values cannot be represented correctly by the narrowed type. For example, `int.MaxValue` cannot be represented by a `short` field, so narrowing an `int` field to `short` can result in a runtime exception if such a value were encountered. -- ❌ **Do not** change the signedness of a numeric type member. You must not change a member's type from `uint` to `int` or an `int` to a `uint`, for example. + - You can narrow numeric member types, but it results in a runtime exception if observed values cannot be represented correctly by the narrowed type. For example, `int.MaxValue` cannot be represented by a `short` field, so narrowing an `int` field to `short` can result in a runtime exception if such a value is encountered. +- ❌ **Do not** change the signedness of a numeric type member. You must not change a member's type from `uint` to `int` or `int` to `uint`, for example. ## Grain storage serializers -Orleans includes a provider-backed persistence model for grains, accessed via the property or by injecting one or more values into your grain. Before Orleans 7.0, each provider had a different mechanism for configuring serialization. In Orleans 7.0, there is now a general-purpose grain state serializer interface, , which offers a consistent way to customize state serialization for each provider. Supported storage providers implement a pattern that involves setting the property on the provider's options class, for example: +Orleans includes a provider-backed persistence model for grains, accessed via the property or by injecting one or more values into your grain. Before Orleans 7.0, each provider had a different mechanism for configuring serialization. In Orleans 7.0, there's now a general-purpose grain state serializer interface, , offering a consistent way to customize state serialization for each provider. Supported storage providers implement a pattern involving setting the property on the provider's options class, for example: - - - - -Grain storage serialization currently defaults to `Newtonsoft.Json` to serialize state. You can replace this by modifying that property at configuration time. The following example demonstrates this, using [OptionsBuilder\](../../../core/extensions/options.md#optionsbuilder-api): +Grain storage serialization currently defaults to `Newtonsoft.Json` to serialize state. You can replace this by modifying that property at configuration time. The following example demonstrates this using [OptionsBuilder\](../../../core/extensions/options.md#optionsbuilder-api): ```csharp siloBuilder.AddAzureBlobGrainStorage( @@ -338,33 +339,32 @@ For more information, see [OptionsBuilder API](../../../core/extensions/options. :::zone target="docs" pivot="orleans-3-x" -Orleans has an advanced and extensible serialization framework. Orleans serializes data types passed in grain request and response messages as well as grain persistent state objects. As part of this framework, Orleans automatically generates serialization code for those data types. In addition to generating a more efficient serialization/deserialization for types that are already .NET-serializable, Orleans also tries to generate serializers for types used in grain interfaces that are not .NET-serializable. The framework also includes a set of efficient built-in serializers for frequently used types: lists, dictionaries, strings, primitives, arrays, etc. +Orleans has an advanced and extensible serialization framework. Orleans serializes data types passed in grain request and response messages, as well as grain persistent state objects. As part of this framework, Orleans automatically generates serialization code for these data types. In addition to generating more efficient serialization/deserialization for types already .NET-serializable, Orleans also tries to generate serializers for types used in grain interfaces that aren't .NET-serializable. The framework also includes a set of efficient built-in serializers for frequently used types: lists, dictionaries, strings, primitives, arrays, etc. -Two important features of Orleans's serializer set it apart from a lot of other third-party serialization frameworks: dynamic types/arbitrary polymorphism and object identity. +Two important features of Orleans' serializer set it apart from many other third-party serialization frameworks: dynamic types/arbitrary polymorphism and object identity. -1. **Dynamic types and arbitrary polymorphism**: Orleans doesn't enforce restrictions on the types that can be passed in grain calls and maintain the dynamic nature of the actual data type. That means, for example, that if the method in the grain interfaces is declared to accept but at runtime, the sender passes , the receiver will indeed get `SortedDictionary` (although the "static contract"/grain interface did not specify this behavior). +1. **Dynamic types and arbitrary polymorphism**: Orleans doesn't enforce restrictions on the types passed in grain calls and maintains the dynamic nature of the actual data type. This means, for example, if a method in a grain interface is declared to accept , but at runtime the sender passes a , the receiver indeed gets a `SortedDictionary` (even though the "static contract"/grain interface didn't specify this behavior). -1. **Maintaining object identity**: If the same object is passed multiple types in the arguments of a grain call or is indirectly pointed more than once from the arguments, Orleans will serialize it only once. On the receiver side, Orleans will restore all references correctly so that two pointers to the same object still point to the same object after deserialization as well. Object identity is important to preserve in scenarios like the following. Imagine grain A is sending a dictionary with 100 entries to grain B, and 10 of the keys in the dictionary point to the same object, obj, on A's side. Without preserving object identity, B would receive a dictionary of 100 entries with those 10 keys pointing to 10 different clones of obj. With object identity-preserved, the dictionary on B's side looks exactly like on A's side with those 10 keys pointing to a single object obj. +1. **Maintaining object identity**: If the same object is passed multiple times in the arguments of a grain call or is indirectly pointed to more than once from the arguments, Orleans serializes it only once. On the receiver side, Orleans restores all references correctly so that two pointers to the same object still point to the same object after deserialization. Preserving object identity is important in scenarios like the following: Imagine grain A sends a dictionary with 100 entries to grain B, and 10 keys in the dictionary point to the same object, `obj`, on A's side. Without preserving object identity, B would receive a dictionary of 100 entries with those 10 keys pointing to 10 different clones of `obj`. With object identity preserved, the dictionary on B's side looks exactly like on A's side, with those 10 keys pointing to a single object `obj`. -The above two behaviors are provided by the standard .NET binary serializer and it was therefore important for us to support this standard and familiar behavior in Orleans as well. +The standard .NET binary serializer provides the above two behaviors, so it was important for us to support this standard and familiar behavior in Orleans as well. ## Generated serializers -Orleans uses the following rules to decide which serializers to generate. -The rules are: +Orleans uses the following rules to decide which serializers to generate: -1. Scan all types in all assemblies which reference the core Orleans library. -1. Out of those assemblies: generate serializers for types that are directly referenced in grain interfaces method signatures or state class signature or for any type that is marked with . -1. In addition, a grain interface or implementation project can point to arbitrary types for serialization generation by adding a or assembly-level attributes to tell code generator to generate serializers for specific types or all eligible types within an assembly. For more information on assembly-level attributes, see [Apply attributes at the assembly level](../../../standard/attributes/applying-attributes.md#apply-attributes-at-the-assembly-level). +1. Scan all types in all assemblies referencing the core Orleans library. +1. From those assemblies, generate serializers for types directly referenced in grain interface method signatures or state class signatures, or for any type marked with . +1. Additionally, a grain interface or implementation project can point to arbitrary types for serialization generation by adding or assembly-level attributes. These tell the code generator to generate serializers for specific types or all eligible types within an assembly. For more information on assembly-level attributes, see [Apply attributes at the assembly level](../../../standard/attributes/applying-attributes.md#apply-attributes-at-the-assembly-level). ## Fallback serialization -Orleans supports transmission of arbitrary types at runtime and therefore the in-built code generator cannot determine the entire set of types that will be transmitted ahead of time. Additionally, certain types cannot have serializers generated for them because they are inaccessible (for example, `private`) or have inaccessible fields (for example, `readonly`). Therefore, there is a need for just-in-time serialization of types that were unexpected or could not have serializers generated ahead of time. The serializer responsible for these types is called the *fallback serializer*. Orleans ships with two fallback serializers: +Orleans supports transmission of arbitrary types at runtime. Therefore, the built-in code generator cannot determine the entire set of types that will be transmitted ahead of time. Additionally, certain types cannot have serializers generated for them because they are inaccessible (e.g., `private`) or have inaccessible fields (e.g., `readonly`). Thus, there's a need for just-in-time serialization of types that were unexpected or couldn't have serializers generated ahead of time. The serializer responsible for these types is called the *fallback serializer*. Orleans ships with two fallback serializers: -* , which uses .NET's ; and -* , which emits [CIL](../../../standard/glossary.md#il) instructions at runtime to create serializers that leverage Orleans' serialization framework to serialize each field. This means that if an inaccessible type `MyPrivateType` contains a field `MyType` which has a custom serializer, that custom serializer will be used to serialize it. +- , which uses .NET's ; and +- , which emits [CIL](../../../standard/glossary.md#il) instructions at runtime to create serializers that leverage Orleans' serialization framework to serialize each field. This means that if an inaccessible type `MyPrivateType` contains a field `MyType` which has a custom serializer, that custom serializer will be used to serialize it. -The fallback serializer can be configured using the property on both on the client and on the silos. +Configure the fallback serializer using the property on both (client) and (silos). ```csharp // Client configuration @@ -378,7 +378,7 @@ globalConfiguration.FallbackSerializationProvider = typeof(FantasticSerializer).GetTypeInfo(); ``` -Alternatively, the fallback serialization provider can be specified in XML configuration: +Alternatively, specify the fallback serialization provider in XML configuration: ```xml @@ -393,7 +393,7 @@ The is the default fallba ## Exception serialization -Exceptions are serialized using the [fallback serializer](serialization.md#fallback-serialization). Using the default configuration, `BinaryFormatter` is the fallback serializer and so the [ISerializable pattern](/previous-versions/dotnet/fundamentals/serialization/binary/custom-serialization) must be followed in order to ensure correct serialization of all properties in an exception type. +Exceptions are serialized using the [fallback serializer](serialization.md#fallback-serialization). With the default configuration, `BinaryFormatter` is the fallback serializer. Therefore, you must follow the [ISerializable pattern](/previous-versions/dotnet/fundamentals/serialization/binary/custom-serialization) to ensure correct serialization of all properties in an exception type. Here is an example of an exception type with correctly implemented serialization: @@ -438,6 +438,6 @@ Serialization serves two primary purposes in Orleans: 1. As a wire format for transmitting data between grains and clients at runtime. 1. As a storage format for persisting long-lived data for later retrieval. -The serializers generated by Orleans are suitable for the first purpose due to their flexibility, performance, and versatility. They are not as suitable for the second purpose, since they are not explicitly version-tolerant. It is recommended that users configure a version-tolerant serializer such as [Protocol Buffers](https://developers.google.com/protocol-buffers/) for persistent data. Protocol Buffers is supported via `Orleans.Serialization.ProtobufSerializer` from the [Microsoft.Orleans.OrleansGoogleUtils](https://www.nuget.org/packages/Microsoft.Orleans.OrleansGoogleUtils/) NuGet package. The best practices for the particular serializer of choice should be used to ensure version tolerance. Third-party serializers can be configured using the `SerializationProviders` configuration property as described above. +The serializers generated by Orleans are suitable for the first purpose due to their flexibility, performance, and versatility. They aren't as suitable for the second purpose since they aren't explicitly version-tolerant. We recommend configuring a version-tolerant serializer, such as [Protocol Buffers](https://developers.google.com/protocol-buffers/), for persistent data. Protocol Buffers is supported via `Orleans.Serialization.ProtobufSerializer` from the [Microsoft.Orleans.OrleansGoogleUtils](https://www.nuget.org/packages/Microsoft.Orleans.OrleansGoogleUtils/) NuGet package. Follow the best practices for your chosen serializer to ensure version tolerance. Configure third-party serializers using the `SerializationProviders` configuration property as described above. :::zone-end diff --git a/docs/orleans/host/configuration-guide/server-configuration.md b/docs/orleans/host/configuration-guide/server-configuration.md index d0ad70a46b2ae..ab86809310876 100644 --- a/docs/orleans/host/configuration-guide/server-configuration.md +++ b/docs/orleans/host/configuration-guide/server-configuration.md @@ -1,7 +1,8 @@ --- title: Server configuration description: Learn how to configure .NET Orleans server settings. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: how-to zone_pivot_groups: orleans-version --- @@ -11,15 +12,15 @@ zone_pivot_groups: orleans-version :::zone target="docs" pivot="orleans-7-0" -A silo is configured programmatically with the extension method and several supplemental option classes. Option classes in Orleans follow the [Options pattern in .NET](../../../core/extensions/options.md), and can be loaded via files, environment variables, and any valid configuration provider. +Configure a silo programmatically using the extension method and several supplemental option classes. Option classes in Orleans follow the [Options pattern in .NET](../../../core/extensions/options.md) and can be loaded from files, environment variables, or any other valid configuration provider. There are several key aspects of silo configuration: - Clustering provider - (Optional) Orleans clustering information -- (Optional) Endpoints to use for silo-to-silo and client-to-silo communications +- (Optional) Endpoints for silo-to-silo and client-to-silo communications -This is an example of a silo configuration that defines cluster information, uses Azure clustering, and configures the application parts: +This example shows a silo configuration defining cluster information, using Azure clustering, and configuring application parts: ```csharp using IHost host = Host.CreateDefaultBuilder(args) @@ -42,11 +43,11 @@ siloBuilder.UseAzureStorageClustering( options => options.ConfigureTableServiceClient(connectionString)) ``` -Usually, a service built on Orleans is deployed on a cluster of nodes, either on dedicated hardware or in the cloud. For development and basic testing, Orleans can be deployed in a single-node configuration. When deployed to a cluster of nodes, Orleans internally implements a set of protocols to discover and maintain membership of Orleans silos in the cluster, including detection of node failures and automatic reconfiguration. +Usually, you deploy a service built on Orleans on a cluster of nodes, either on dedicated hardware or in the cloud. For development and basic testing, you can deploy Orleans in a single-node configuration. When deployed to a cluster of nodes, Orleans internally implements protocols to discover and maintain membership of Orleans silos in the cluster, including detecting node failures and automatic reconfiguration. -For reliable management of cluster membership, Orleans uses Azure Table, SQL Server, or Apache ZooKeeper for the synchronization of nodes. +For reliable cluster membership management, Orleans uses Azure Table, SQL Server, or Apache ZooKeeper for node synchronization. -In this sample, Azure Table as the membership provider is used. +In this sample, we use Azure Table as the membership provider. ## Orleans clustering information @@ -60,20 +61,20 @@ siloBuilder.Configure(options => }) ``` -Here you specify two options: +Here, you specify two options: -- Set the `ClusterId` to `"my-first-cluster"`: this is a unique ID for the Orleans cluster. All clients and silos that use this ID will be able to talk directly to each other. You can choose to use a different `ClusterId` for different deployments, though. -- Set the `ServiceId` to `"SampleApp"`: this is a unique ID for your application that will be used by some providers, such as persistence providers. **This ID should remain stable and not change across deployments**. +- Set the `ClusterId` to `"my-first-cluster"`: This is a unique ID for the Orleans cluster. All clients and silos using this ID can talk directly to each other. You can choose to use a different `ClusterId` for different deployments, though. +- Set the `ServiceId` to `"SampleApp"`: This is a unique ID for your application used by some providers, such as persistence providers. **This ID should remain stable and not change across deployments**. -By default, Orleans will use a value of `"default"` for both the `ServiceId` and the `ClusterId`. These values don't need to be changed in most cases. `ServiceId` is the more significant of the two, and is used to distinguish different logical services from each other so that they can share backend storage systems without interfering with each other. `ClusterId` is used to determine which hosts will connect to each other and form a cluster. +By default, Orleans uses `"default"` for both `ServiceId` and `ClusterId`. These values don't need changing in most cases. `ServiceId` is more significant and distinguishes different logical services, allowing them to share backend storage systems without interference. `ClusterId` determines which hosts connect to form a cluster. -Within each cluster, all hosts must use the same `ServiceId`. Multiple clusters can share a `ServiceId`, however. This enables blue/green deployment scenarios where a new deployment (cluster) is started before another is shut down. This is typical for systems hosted in Azure App Service. +Within each cluster, all hosts must use the same `ServiceId`. However, multiple clusters can share a `ServiceId`. This enables blue/green deployment scenarios where you start a new deployment (cluster) before shutting down another. This is typical for systems hosted in Azure App Service. -The more common case is that `ServiceId` and `ClusterId` remain fixed for the lifetime of the application and a rolling deployment strategy is used. This is typical for systems hosted in Kubernetes and Service Fabric. +The more common case is that `ServiceId` and `ClusterId` remain fixed for the application's lifetime, and you use a rolling deployment strategy. This is typical for systems hosted in Kubernetes and Service Fabric. ## Endpoints -By default, Orleans will listen on all interfaces on port `11111` for silo-to-silo communication and on port `30000` for client-to-silo communication. To override this behavior, call and pass in the port numbers you want to use. +By default, Orleans listens on all interfaces on port `11111` for silo-to-silo communication and port `30000` for client-to-silo communication. To override this behavior, call and pass in the port numbers you want to use. ```csharp siloBuilder.ConfigureEndpoints(siloPort: 17_256, gatewayPort: 34_512) @@ -86,11 +87,10 @@ In the preceding code: An Orleans silo has two typical types of endpoint configuration: -- Silo-to-silo endpoints are used for communication between silos in the same cluster. -- Client-to-silo (or gateway) endpoints are used for communication between clients and silos in the same cluster. +- Silo-to-silo endpoints: Used for communication between silos in the same cluster. +- Client-to-silo (or gateway) endpoints: Used for communication between clients and silos in the same cluster. -This method should be sufficient in most cases, but you can customize it further if you need to. -Here is an example of how to use an external IP address with some port-forwarding: +This method should suffice in most cases, but you can customize it further if needed. Here's an example of using an external IP address with port forwarding: ```csharp siloBuilder.Configure(options => @@ -108,7 +108,7 @@ siloBuilder.Configure(options => }) ``` -Internally, the silo will listen on `0.0.0.0:40000` and `0.0.0.0:50000`, but the value published in the membership provider will be `172.16.0.42:11111` and `172.16.0.42:30000`. +Internally, the silo listens on `0.0.0.0:40000` and `0.0.0.0:50000`, but the value published in the membership provider is `172.16.0.42:11111` and `172.16.0.42:30000`. :::zone-end @@ -116,16 +116,16 @@ Internally, the silo will listen on `0.0.0.0:40000` and `0.0.0.0:50000`, but the :::zone target="docs" pivot="orleans-3-x" -A silo is configured programmatically via and several supplemental option classes. Option classes in Orleans follow the [Options pattern in .NET](../../../core/extensions/options.md), and can be loaded via files, environment variables, and any valid configuration provider. +Configure a silo programmatically via and several supplemental option classes. Option classes in Orleans follow the [Options pattern in .NET](../../../core/extensions/options.md) and can be loaded from files, environment variables, or any other valid configuration provider. There are several key aspects of silo configuration: - Orleans clustering information - Clustering provider -- Endpoints to use for silo-to-silo and client-to-silo communications +- Endpoints for silo-to-silo and client-to-silo communications - Application parts -This is an example of a silo configuration that defines cluster information, uses Azure clustering, and configures the application parts: +This example shows a silo configuration defining cluster information, using Azure clustering, and configuring application parts: ```csharp var silo = Host.CreateDefaultBuilder(args) @@ -147,7 +147,7 @@ var silo = Host.CreateDefaultBuilder(args) .Build(); ``` -Let's breakdown the steps used in this sample: +Let's break down the steps used in this sample: ## Clustering provider @@ -156,11 +156,11 @@ siloBuilder.UseAzureStorageClustering( options => options.ConnectionString = connectionString) ``` -Usually, a service built on Orleans is deployed on a cluster of nodes, either on dedicated hardware or in the cloud. For development and basic testing, Orleans can be deployed in a single-node configuration. When deployed to a cluster of nodes, Orleans internally implements a set of protocols to discover and maintain membership of Orleans silos in the cluster, including detection of node failures and automatic reconfiguration. +Usually, you deploy a service built on Orleans on a cluster of nodes, either on dedicated hardware or in the cloud. For development and basic testing, you can deploy Orleans in a single-node configuration. When deployed to a cluster of nodes, Orleans internally implements protocols to discover and maintain membership of Orleans silos in the cluster, including detecting node failures and automatic reconfiguration. -For reliable management of cluster membership, Orleans uses Azure Table, SQL Server, or Apache ZooKeeper for the synchronization of nodes. +For reliable cluster membership management, Orleans uses Azure Table, SQL Server, or Apache ZooKeeper for node synchronization. -In this sample, we are using Azure Table as the membership provider. +In this sample, we use Azure Table as the membership provider. ## Orleans clustering information @@ -172,16 +172,16 @@ In this sample, we are using Azure Table as the membership provider. }) ``` -Here we do two things: +Here, we do two things: -- Set the `ClusterId` to `"my-first-cluster"`: this is a unique ID for the Orleans cluster. All clients and silos that use this ID will be able to talk directly to each other. You can choose to use a different `ClusterId` for different deployments, though. -- Set the `ServiceId` to `"AspNetSampleApp"`: this is a unique ID for your application that will be used by some providers, such as persistence providers. **This ID should remain stable and not change across deployments**. +- Set the `ClusterId` to `"my-first-cluster"`: This is a unique ID for the Orleans cluster. All clients and silos using this ID can talk directly to each other. You can choose to use a different `ClusterId` for different deployments, though. +- Set the `ServiceId` to `"AspNetSampleApp"`: This is a unique ID for your application used by some providers, such as persistence providers. **This ID should remain stable and not change across deployments**. -By default, Orleans will use a value of `"default"` for both the `ServiceId` and the `ClusterId`. These values don't need to be changed in most cases. `ServiceId` is the more significant of the two, and is used to distinguish different logical services from each other so that they can share backend storage systems without interfering with each other. `ClusterId` is used to determine which hosts will connect to each other and form a cluster. +By default, Orleans uses `"default"` for both `ServiceId` and `ClusterId`. These values don't need changing in most cases. `ServiceId` is more significant and distinguishes different logical services, allowing them to share backend storage systems without interference. `ClusterId` determines which hosts connect to form a cluster. -Within each cluster, all hosts must use the same `ServiceId`. Multiple clusters can share a `ServiceId`, however. This enables blue/green deployment scenarios where a new deployment (cluster) is started before another is shut down. This is typical for systems hosted in Azure App Service. +Within each cluster, all hosts must use the same `ServiceId`. However, multiple clusters can share a `ServiceId`. This enables blue/green deployment scenarios where you start a new deployment (cluster) before shutting down another. This is typical for systems hosted in Azure App Service. -The more common case is that `ServiceId` and `ClusterId` remain fixed for the lifetime of the application and a rolling deployment strategy is used. This is typical for systems hosted in Kubernetes and Service Fabric. +The more common case is that `ServiceId` and `ClusterId` remain fixed for the application's lifetime, and you use a rolling deployment strategy. This is typical for systems hosted in Kubernetes and Service Fabric. ## Endpoints @@ -191,14 +191,12 @@ siloBuilder.ConfigureEndpoints(siloPort: 11111, gatewayPort: 30000) An Orleans silo has two typical types of endpoint configuration: -- Silo-to-silo endpoints, used for communication between silos in the same cluster -- Client-to-silo endpoints (or gateway), used for communication between clients and silos in the same cluster +- Silo-to-silo endpoints: Used for communication between silos in the same cluster. +- Client-to-silo endpoints (or gateway): Used for communication between clients and silos in the same cluster. -In the sample, we are using the helper method `.ConfigureEndpoints(siloPort: 11111, gatewayPort: 30000)` which sets the port used for silo-to-silo communication to `11111` and the port for the gateway to `30000`. -This method will detect which interface to listen to. +In the sample, we use the helper method `.ConfigureEndpoints(siloPort: 11111, gatewayPort: 30000)`, which sets the port for silo-to-silo communication to `11111` and the gateway port to `30000`. This method detects which interface to listen on. -This method should be sufficient in most cases, but you can customize it further if you need to. -Here is an example of how to use an external IP address with some port-forwarding: +This method should suffice in most cases, but you can customize it further if needed. Here's an example of using an external IP address with port forwarding: ```csharp siloBuilder.Configure(options => @@ -216,7 +214,7 @@ siloBuilder.Configure(options => }) ``` -Internally, the silo will listen on `0.0.0.0:40000` and `0.0.0.0:50000`, but the value published in the membership provider will be `172.16.0.42:11111` and `172.16.0.42:30000`. +Internally, the silo listens on `0.0.0.0:40000` and `0.0.0.0:50000`, but the value published in the membership provider is `172.16.0.42:11111` and `172.16.0.42:30000`. ## Application parts @@ -227,21 +225,21 @@ siloBuilder.ConfigureApplicationParts( .WithReferences()) ``` -Although this step is not technically required (if not configured, Orleans will scan all assemblies in the current folder), developers are encouraged to configure this. This step will help Orleans to load user assemblies and types. These assemblies are referred to as Application Parts. All Grains, Grain Interfaces, and Serializers are discovered using Application Parts. +Although this step isn't technically required (if not configured, Orleans scans all assemblies in the current folder), we encourage you to configure it. This step helps Orleans load user assemblies and types. These assemblies are referred to as Application Parts. Orleans discovers all Grains, Grain Interfaces, and Serializers using Application Parts. -Application Parts are configured using , which can be accessed using the `ConfigureApplicationParts` extension method on and . The `ConfigureApplicationParts` method accepts a delegate, `Action`. +Configure Application Parts using , accessible via the `ConfigureApplicationParts` extension method on and . The `ConfigureApplicationParts` method accepts a delegate, `Action`. The following extension methods on support common uses: -- a single assembly can be added using this extension method. -- adds all assemblies currently loaded in the `AppDomain`. -- loads and adds all assemblies in the current base path (see ). +- : Add a single assembly using this extension method. +- : Adds all assemblies currently loaded in the `AppDomain`. +- : Loads and adds all assemblies in the current base path (see ). -Assemblies added by the above methods can be supplemented using the following extension methods on their return type, : +Supplement assemblies added by the above methods using the following extension methods on their return type, : -- adds all referenced assemblies from the added parts. This immediately loads any transitively referenced assemblies. Assembly loading errors are ignored. -- generates support code for the added parts and adds it to the part manager. Note that this requires the `Microsoft.Orleans.OrleansCodeGenerator` package to be installed and is commonly referred to as runtime code generation. +- : Adds all referenced assemblies from the added parts. This immediately loads any transitively referenced assemblies. Assembly loading errors are ignored. +- : Generates support code for the added parts and adds it to the part manager. Note that this requires installing the `Microsoft.Orleans.OrleansCodeGenerator` package and is commonly referred to as runtime code generation. -Type discovery requires that the provided Application Parts include specific attributes. Adding the build-time code generation package (`Microsoft.Orleans.CodeGenerator.MSBuild` or `Microsoft.Orleans.OrleansCodeGenerator.Build`) to each project containing Grains, Grain Interfaces, or Serializers is the recommended approach for ensuring that these attributes are present. Build-time code generation only supports C#. For F#, Visual Basic, and other .NET languages, code can be generated during configuration time via the method described above. More info regarding code generation could be found in [the corresponding section](../../grains/code-generation.md). +Type discovery requires the provided Application Parts to include specific attributes. Adding the build-time code generation package (`Microsoft.Orleans.CodeGenerator.MSBuild` or `Microsoft.Orleans.OrleansCodeGenerator.Build`) to each project containing Grains, Grain Interfaces, or Serializers is the recommended approach to ensure these attributes are present. Build-time code generation only supports C#. For F#, Visual Basic, and other .NET languages, you can generate code during configuration time via the method described above. Find more info regarding code generation in [the corresponding section](../../grains/code-generation.md). :::zone-end diff --git a/docs/orleans/host/configuration-guide/shutting-down-orleans.md b/docs/orleans/host/configuration-guide/shutting-down-orleans.md index ae03f3966bb9d..8595d56a9d460 100644 --- a/docs/orleans/host/configuration-guide/shutting-down-orleans.md +++ b/docs/orleans/host/configuration-guide/shutting-down-orleans.md @@ -1,14 +1,15 @@ --- title: Shut down Orleans silos description: Learn how to shut down .NET Orleans silos. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: how-to --- # Shut down Orleans silos -This article explains how to gracefully shut down an Orleans silo before the app exits. This applies to apps running as a console app, or as a container app. Various termination signals can cause an app to shut down, such as Ctrl+C (or `SIGTERM`). The following sections explain how to handle these signals. +This article explains how to gracefully shut down an Orleans silo before the app exits. This applies to apps running as console apps or container apps. Various termination signals can cause an app to shut down, such as Ctrl+C (or `SIGTERM`). The following sections explain how to handle these signals. -## Graceful Silo shutdown +## Graceful silo shutdown The following code shows how to gracefully shut down an Orleans silo console app. Consider the following example code: @@ -25,9 +26,9 @@ await Host.CreateDefaultBuilder(args) .RunConsoleAsync(); ``` -The preceding code relies on the [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting), and [Microsoft.Orleans.Server](https://www.nuget.org/packages/Microsoft.Orleans.Server) NuGet packages. The extension method, extends to help manage the lifetime of the app accordingly, listening for process termination signals and shutting down the silo gracefully. +The preceding code relies on the [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting) and [Microsoft.Orleans.Server](https://www.nuget.org/packages/Microsoft.Orleans.Server) NuGet packages. The extension method extends to help manage the app's lifetime accordingly, listening for process termination signals and shutting down the silo gracefully. -Internally, the `RunConsoleAsync` method calls which ensures that the app shuts down gracefully. For more information on host shutdown, see [.NET Generic Host: Host shutdown](../../../core/extensions/generic-host.md#host-shutdown). +Internally, the `RunConsoleAsync` method calls , which ensures the app shuts down gracefully. For more information on host shutdown, see [.NET Generic Host: Host shutdown](../../../core/extensions/generic-host.md#host-shutdown). ## See also diff --git a/docs/orleans/host/configuration-guide/startup-tasks.md b/docs/orleans/host/configuration-guide/startup-tasks.md index d553ac1cf520b..52cc239aec14f 100644 --- a/docs/orleans/host/configuration-guide/startup-tasks.md +++ b/docs/orleans/host/configuration-guide/startup-tasks.md @@ -1,24 +1,24 @@ --- title: Background Services and Startup Tasks description: Learn how to configure and manage background services and startup tasks in .NET Orleans. -ms.date: 11/19/2024 +ms.date: 05/23/2025 --- -# Background Services and Startup Tasks +# Background services and startup tasks When building Orleans applications, you often need to perform background operations or initialize components when the application starts. -Startup tasks can be used to perform initialization work when a silo starts, before or after it begins accepting requests. Common use cases include: +Use startup tasks to perform initialization work when a silo starts, either before or after it begins accepting requests. Common use cases include: -* Initializing grain state or preloading data -* Setting up external service connections -* Performing database migrations -* Validating configuration -* Warming up caches +- Initializing grain state or preloading data +- Setting up external service connections +- Performing database migrations +- Validating configuration +- Warming up caches -## Using BackgroundService (Recommended) +## Using `BackgroundService` (Recommended) -The recommended approach is to use .NET [BackgroundService or `IHostedService`](/aspnet/core/fundamentals/host/hosted-services). See the [Background tasks with hosted services in ASP.NET Core](/aspnet/core/fundamentals/host/hosted-services) documentation for more information. +The recommended approach is to use .NET [`BackgroundService` or `IHostedService`](/aspnet/core/fundamentals/host/hosted-services). See the [Background tasks with hosted services in ASP.NET Core](/aspnet/core/fundamentals/host/hosted-services) documentation for more information. Here's an example that pings a grain every 5 seconds: @@ -69,7 +69,7 @@ public class GrainPingService : BackgroundService } ``` -Registration order is significant, since services added to the host builder are started one-by-one, in the order they are registered. You can register the background service as follows: +Registration order is significant because services added to the host builder start one by one in the order they are registered. Register the background service as follows: ```csharp var builder = WebApplication.CreateBuilder(args); @@ -86,11 +86,11 @@ builder.Services.AddHostedService(); var app = builder.Build(); ``` -The background service will start automatically when the application starts and will gracefully shut down when the application stops. +The background service starts automatically when the application starts and gracefully shuts down when the application stops. -## Using IHostedService +## Using `IHostedService` -For simpler scenarios where you don't need continuous background operation, you can implement `IHostedService` directly: +For simpler scenarios where you don't need continuous background operation, implement `IHostedService` directly: ```csharp public class GrainInitializerService : IHostedService @@ -126,19 +126,19 @@ Register it the same way: builder.Services.AddHostedService(); ``` -## Orleans' Startup Tasks +## Orleans startup tasks > [!NOTE] -> While startup tasks are still supported, we recommend using `BackgroundService` or `IHostedService` instead as they are the common .NET hosting mechanism for running background tasks. +> While startup tasks are still supported, we recommend using `BackgroundService` or `IHostedService` instead, as they are the standard .NET hosting mechanism for running background tasks. > [!WARNING] -> Any exceptions thrown from a startup task will be reported in the silo log and will stop the silo. This fail-fast approach helps detect configuration and bootstrap issues during testing rather than having them cause unexpected problems later, but it can also mean that transient failures in a startup task will cause unavailability of the host. +> Any exceptions thrown from a startup task are reported in the silo log and stop the silo. This fail-fast approach helps detect configuration and bootstrap issues during testing rather than causing unexpected problems later. However, it can also mean that transient failures in a startup task cause host unavailability. -If you need to use the built-in startup task system, you can configure them as follows: +If you need to use the built-in startup task system, configure them as follows: ### Register a delegate -A delegate can be registered as a startup task using the appropriate extension method on . +Register a delegate as a startup task using the appropriate extension method on . ```csharp siloBuilder.AddStartupTask( @@ -152,7 +152,7 @@ siloBuilder.AddStartupTask( ### Register an `IStartupTask` implementation -The interface can be implemented and registered as a startup task using the extension method on . +Implement the interface and register it as a startup task using the extension method on . ```csharp public class CallGrainStartupTask : IStartupTask diff --git a/docs/orleans/host/configuration-guide/typical-configurations.md b/docs/orleans/host/configuration-guide/typical-configurations.md index bb9dbefc6636b..af0e359020f16 100644 --- a/docs/orleans/host/configuration-guide/typical-configurations.md +++ b/docs/orleans/host/configuration-guide/typical-configurations.md @@ -1,12 +1,13 @@ --- title: Typical configurations description: Learn about typical configurations in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: reference --- # Typical configurations -Below are examples of typical configurations that can be used for development and production deployments. +Below are examples of typical configurations you can use for development and production deployments. ## Local development @@ -14,9 +15,9 @@ For more information, see [Local development configuration](local-development-co ## Reliable production deployment using Azure -For a reliable production deployment using Azure, you need to use the Azure Table option for cluster membership. This configuration is typical of deployments to either on-premises servers, containers, or Azure virtual machine instances. +For a reliable production deployment using Azure, use the Azure Table option for cluster membership. This configuration is typical for deployments to on-premises servers, containers, or Azure virtual machine instances. - The format of the `DataConnection` string is a `;` separated list of `Key=Value` pairs. The following options are supported: + The format of the `DataConnection` string is a semicolon-separated list of `Key=Value` pairs. The following options are supported: | Key | Value | |----------------------------|-------------------------------------| @@ -69,7 +70,7 @@ using var host = Host.CreateDefaultBuilder(args) ## Reliable production deployment using SQL Server -For a reliable production deployment using SQL server, a SQL server connection string needs to be supplied. +For a reliable production deployment using SQL Server, supply a SQL Server connection string. Silo configuration: @@ -116,7 +117,7 @@ using var host = Host.CreateDefaultBuilder(args) ## Unreliable deployment on a cluster of dedicated servers -For testing on a cluster of dedicated servers when reliability isn't a concern you can leverage `MembershipTableGrain` and avoid dependency on Azure Table. You just need to designate one of the nodes as a primary. +For testing on a cluster of dedicated servers where reliability isn't a concern, you can leverage `MembershipTableGrain` and avoid dependency on Azure Table. You just need to designate one of the nodes as primary. On the silos: diff --git a/docs/orleans/host/grain-directory.md b/docs/orleans/host/grain-directory.md index 2277187cc2ad0..2d25e17a80a48 100644 --- a/docs/orleans/host/grain-directory.md +++ b/docs/orleans/host/grain-directory.md @@ -1,16 +1,17 @@ --- title: Grain directory description: Learn about the grain directory in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Orleans grain directory -Grains have stable logical identities and may get activated (instantiated) and deactivated many times over the life of the application, but at most one activation of grain exist at any point in time. Each time a grain gets activated, it may be placed on a different silo in the cluster. When a grain gets activated in the cluster, it registers itself in the _grain directory_. This ensures that subsequent invocations of that grain will be delivered to that activation of the grain and that no other activations (instances) of that grain will be created. The grain directory is responsible for keeping a mapping between a grain identity and where (which silo) its current activation is at. +Grains have stable logical identities. They can activate (instantiate) and deactivate many times over the application's life, but at most one activation of a grain exists at any point in time. Each time a grain activates, it might be placed on a different silo in the cluster. When a grain activates in the cluster, it registers itself in the _grain directory_. This ensures subsequent invocations of that grain are delivered to that activation and prevents the creation of other activations (instances) of that grain. The grain directory is responsible for maintaining a mapping between a grain identity and the location (which silo) of its current activation. -By default, Orleans uses a built-in distributed in-memory directory. This directory is eventually consistent and partitioned across all silos in the cluster in a form of a distributed hash table. +By default, Orleans uses a built-in distributed in-memory directory. This directory is eventually consistent and partitioned across all silos in the cluster in the form of a distributed hash table. -Starting with 3.2.0, Orleans also supports pluggable implementations of grain directory. +Starting with version 3.2.0, Orleans also supports pluggable grain directory implementations. Two such plugins are included in the 3.2.0 release: @@ -21,17 +22,17 @@ You can configure which grain directory implementation to use on a per-grain typ ## Which grain directory should you use? -We recommend always starting with the default one (built-in in-memory distributed directory). Even though it is eventually consistent and allows for occasional duplicate activation when the cluster is unstable, the built-in directory is self-sufficient with no external dependencies, doesn't require any configuration, and has been used in production the whole time. +We recommend always starting with the default directory (the built-in in-memory distributed directory). Although it's eventually consistent and allows occasional duplicate activations when the cluster is unstable, the built-in directory is self-sufficient, has no external dependencies, requires no configuration, and has been used successfully in production since the beginning. -When you have some experience with Orleans and have a use case for a grain directory with stronger single-activation guarantee and/or want to minimize the number of grain that gets deactivated when a silo in the cluster shuts down, consider using a storage-based implementation of grain directory, such as the Redis implementation. Try using it for one or a few grain types first, starting with those that are long-lived and have a significant amount of state or an expensive initialization process. +When you have some experience with Orleans and have a use case requiring a stronger single-activation guarantee, or if you want to minimize the number of grains deactivated when a silo shuts down, consider using a storage-based grain directory implementation, such as the Redis implementation. Try using it for one or a few grain types first, starting with those that are long-lived, have significant state, or have an expensive initialization process. ## Configuration -By default, you don't have to do anything; the in-memory grain directory will be automatically used and partitioned across the cluster. If you'd like to use a non-default grain directory configuration, you need to specify the name of the directory plugin to use. This can be done via an attribute on the grain class and with dependency injection and the directory plugin with that name during the silo configuration. +By default, you don't need to do anything; Orleans automatically uses the in-memory grain directory and partitions it across the cluster. If you want to use a non-default grain directory configuration, you need to specify the name of the directory plugin to use. You can do this via an attribute on the grain class and by configuring the directory plugin with that name using dependency injection during silo configuration. ### Grain configuration -Specifying the grain directory plugin name with the : +Specify the grain directory plugin name using the : ```csharp [GrainDirectory(GrainDirectoryName = "my-grain-directory")] @@ -43,7 +44,7 @@ public class MyGrain : Grain, IMyGrain #### Silo configuration -Here we configure the Redis grain directory implementation: +Here's how you configure the Redis grain directory implementation: ```csharp siloBuilder.AddRedisGrainDirectory( @@ -51,7 +52,7 @@ siloBuilder.AddRedisGrainDirectory( options => options.ConfigurationOptions = redisConfiguration); ``` -The Azure grain directory is configured like this: +Configure the Azure grain directory like this: ```csharp siloBuilder.AddAzureTableGrainDirectory( @@ -59,7 +60,7 @@ siloBuilder.AddAzureTableGrainDirectory( options => options.ConnectionString = azureConnectionString); ``` -You can configure multiple directories with different names to use for different grain classes: +You can configure multiple directories with different names for use with different grain classes: ```csharp siloBuilder diff --git a/docs/orleans/host/heterogeneous-silos.md b/docs/orleans/host/heterogeneous-silos.md index b9a609c39b7b6..004df5a8b5734 100644 --- a/docs/orleans/host/heterogeneous-silos.md +++ b/docs/orleans/host/heterogeneous-silos.md @@ -1,23 +1,24 @@ --- title: Heterogeneous silos overview -description: Learn an overview of the supported heterogeneous silos in .NET Orleans. -ms.date: 07/03/2024 +description: Learn an overview of heterogeneous silos in .NET Orleans. +ms.date: 05/23/2025 +ms.topic: overview --- # Heterogeneous silos overview -On a given cluster, silos can support a different set of grain types: +On a given cluster, silos can support different sets of grain types: :::image type="content" source="media/heterogeneous.png" alt-text="Heterogeneous silos overview diagram."::: -In this example the cluster supports grains of type `A`, `B`, `C`, `D`, `E`: +In this example, the cluster supports grains of type `A`, `B`, `C`, `D`, and `E`: -* Grain types `A` and `B` can be placed on Silo 1 and 2. -* Grain type `C` can be placed on Silo 1, 2, or 3. -* Grain type `D` can be only placed on Silo 3 -* Grain Type `E` can be only placed on Silo 4. +- Grain types `A` and `B` can be placed on Silo 1 and 2. +- Grain type `C` can be placed on Silo 1, 2, or 3. +- Grain type `D` can only be placed on Silo 3. +- Grain type `E` can only be placed on Silo 4. -All silos should reference interfaces of all grain types of the cluster, but grain classes should only be referenced by the silos that will host them. The client does not know which silo supports a given Grain Type. +All silos should reference the interfaces of all grain types in the cluster, but grain classes should only be referenced by the silos that host them. The client doesn't know which silo supports a given grain type. > [!IMPORTANT] > A given grain type implementation must be the same on each silo that supports it. @@ -45,14 +46,14 @@ public class C: Grain, IMyGrainInterface, IMyOtherGrainInterface ## Configuration -No configuration is needed, you can deploy different binaries on each silo in your cluster. However, if necessary, you can change the interval that silos and clients check for changes in types supported with the property. +No configuration is needed; you can deploy different binaries on each silo in your cluster. However, if necessary, you can change the interval at which silos and clients check for changes in supported types using the property. -For testing purposes, you can use the property , which is a list of names of the types you want to exclude on the silos. +For testing purposes, you can use the property, which is a list of names of the types you want to exclude on specific silos. ## Limitations -* Connected clients will not be notified if the set of supported Grain Types changed. In the previous example: - * If Silo 4 leaves the cluster, the client will still try to make calls to the grain of type `E`. It will fail at runtime with an . - * If the client was connected to the cluster before Silo 4 joined it, the client will not be able to make calls to the grain of type `E`. It will fail with an . -* Stateless grains are not supported: all silos in the cluster must support the same set of stateless grains. -* are not supported and thus only [Explicit subscriptions](../streaming/streams-programming-apis.md) can be used in Orleans Streams. +- Connected clients aren't notified if the set of supported grain types changes. In the previous example: + - If Silo 4 leaves the cluster, the client still tries to make calls to grains of type `E`. It fails at runtime with an . + - If the client connected to the cluster before Silo 4 joined, the client cannot make calls to grains of type `E`. It fails with an . +- Stateless grains aren't supported in heterogeneous deployments: all silos in the cluster must support the same set of stateless grains. +- isn't supported; thus, you can only use [Explicit subscriptions](../streaming/streams-programming-apis.md) in Orleans Streams with heterogeneous silos. diff --git a/docs/orleans/host/monitoring/client-error-code-monitoring.md b/docs/orleans/host/monitoring/client-error-code-monitoring.md index 8bf6597d62517..9190f2221a9b3 100644 --- a/docs/orleans/host/monitoring/client-error-code-monitoring.md +++ b/docs/orleans/host/monitoring/client-error-code-monitoring.md @@ -1,7 +1,7 @@ --- title: Client error code monitoring description: Explore the various client error code monitoring values in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 ms.topic: error-reference --- diff --git a/docs/orleans/host/monitoring/index.md b/docs/orleans/host/monitoring/index.md index 745a233fa04fb..3d296c2e00dee 100644 --- a/docs/orleans/host/monitoring/index.md +++ b/docs/orleans/host/monitoring/index.md @@ -1,17 +1,18 @@ --- title: Orleans observability description: Explore the various runtime monitoring, logging, distributed tracing, and metrics options available in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: overview zone_pivot_groups: orleans-version --- # Orleans observability -One of the most important aspects of a distributed system is observability. Observability is the ability to understand the state of the system at any given time. There are various ways to achieve this, including logging, metrics, and distributed tracing. +Observability is one of the most important aspects of a distributed system. It's the ability to understand the system's state at any given time. You can achieve this in various ways, including logging, metrics, and distributed tracing. ## Logging -Orleans uses [Microsoft.Extensions.Logging](https://www.nuget.org/packages/Microsoft.Extensions.Logging) for all silo and client logs. You can use any logging provider that is compatible with `Microsoft.Extensions.Logging`. Your app code would rely on [dependency injection](../../../core/extensions/dependency-injection.md) to get an instance of and use it to log messages. For more information, see [Logging in .NET](../../../core/extensions/logging.md). +Orleans uses [Microsoft.Extensions.Logging](https://www.nuget.org/packages/Microsoft.Extensions.Logging) for all silo and client logs. You can use any logging provider compatible with `Microsoft.Extensions.Logging`. Your app code relies on [dependency injection](../../../core/extensions/dependency-injection.md) to get an instance of and uses it to log messages. For more information, see [Logging in .NET](../../../core/extensions/logging.md). :::zone target="docs" pivot="orleans-7-0" @@ -19,15 +20,15 @@ Orleans uses [Microsoft.Extensions.Logging](https://www.nuget.org/packages/Micro ## Metrics -Metrics are numerical measurements reported over time. They're most often used to monitor the health of an application and generate alerts. For more information, see [Metrics in .NET](../../../core/diagnostics/metrics.md). Orleans uses the [System.Diagnostics.Metrics](../../../core/diagnostics/compare-metric-apis.md#systemdiagnosticsmetrics) APIs to collect metrics. The metrics are exposed to the [OpenTelemetry](https://opentelemetry.io) project, which exports the metrics to various monitoring systems. +Metrics are numerical measurements reported over time. You most often use them to monitor an application's health and generate alerts. For more information, see [Metrics in .NET](../../../core/diagnostics/metrics.md). Orleans uses the [System.Diagnostics.Metrics](../../../core/diagnostics/compare-metric-apis.md#systemdiagnosticsmetrics) APIs to collect metrics. These metrics are exposed to the [OpenTelemetry](https://opentelemetry.io) project, which exports them to various monitoring systems. -To monitor your app without making any code changes at all, you can use the `dotnet counters` .NET diagnostic tool. To monitor Orleans counters, given your desired `` to monitor, use the `dotnet counters monitor` command as shown: +To monitor your app without making any code changes, use the `dotnet counters` .NET diagnostic tool. To monitor Orleans counters for a specific ``, use the `dotnet counters monitor` command as shown: ```dotnetcli dotnet counters monitor -n --counters Microsoft.Orleans ``` -Imagine that you're running the [Orleans GPS Tracker sample app](/samples/dotnet/samples/orleans-gps-device-tracker-sample), and in a separate terminal, you're monitoring it with the `dotnet counters monitor` command. The following output is typical: +Imagine you're running the [Orleans GPS Tracker sample app](/samples/dotnet/samples/orleans-gps-device-tracker-sample) and monitoring it in a separate terminal with the `dotnet counters monitor` command. The following output is typical: ```dotnetcli Press p to pause, r to resume, q to quit. @@ -81,11 +82,11 @@ For more information, see [Investigate performance counters (dotnet-counters)](. ### Orleans meters -Orleans uses the [System.Diagnostics.Metrics](../../../core/diagnostics/compare-metric-apis.md#systemdiagnosticsmetrics) APIs to collect metrics. Orleans categorizes each meter into domain-centric concerns, such as networking, messaging, gateway, and so on. The following subsections describe the meters that Orleans uses. +Orleans uses the [System.Diagnostics.Metrics](../../../core/diagnostics/compare-metric-apis.md#systemdiagnosticsmetrics) APIs to collect metrics. Orleans categorizes each meter into domain-centric concerns, such as networking, messaging, gateway, etc. The following subsections describe the meters Orleans uses. #### Networking -The following table represents a collection of networking meters that are used to monitor the Orleans networking layer. +The following table shows a collection of networking meters used to monitor the Orleans networking layer. | Meter name | Type | Description | |--|--|--| @@ -94,7 +95,7 @@ The following table represents a collection of networking meters that are used t #### Messaging -The following table represents a collection of messaging meters that are used to monitor the Orleans messaging layer. +The following table shows a collection of messaging meters used to monitor the Orleans messaging layer. | Meter name | Type | Description | |--|--|--| @@ -121,7 +122,7 @@ The following table represents a collection of messaging meters that are used to #### Gateway -The following table represents a collection of gateway meters that are used to monitor the Orleans gateway layer. +The following table shows a collection of gateway meters used to monitor the Orleans gateway layer. | Meter name | Type | Description | |--|--|--| @@ -132,7 +133,7 @@ The following table represents a collection of gateway meters that are used to m #### Runtime -The following table represents a collection of runtime meters that are used to monitor the Orleans runtime layer. +The following table shows a collection of runtime meters used to monitor the Orleans runtime layer. | Meter name | Type | Description | |--|--|--| @@ -142,7 +143,7 @@ The following table represents a collection of runtime meters that are used to m #### Catalog -The following table represents a collection of catalog meters that are used to monitor the Orleans catalog layer. +The following table shows a collection of catalog meters used to monitor the Orleans catalog layer. | Meter name | Type | Description | |--|--|--| @@ -158,7 +159,7 @@ The following table represents a collection of catalog meters that are used to m #### Directory -The following table represents a collection of directory meters that are used to monitor the Orleans directory layer. +The following table shows a collection of directory meters used to monitor the Orleans directory layer. | Meter name | Type | Description | |--|--|--| @@ -193,7 +194,7 @@ The following table represents a collection of directory meters that are used to #### Consistent ring -The following table represents a collection of consistent ring meters that are used to monitor the Orleans consistent ring layer. +The following table shows a collection of consistent ring meters used to monitor the Orleans consistent ring layer. | Meter name | Type | Description | |--|--|--| @@ -203,7 +204,7 @@ The following table represents a collection of consistent ring meters that are u #### Watchdog -The following table represents a collection of watchdog meters that are used to monitor the Orleans watchdog layer. +The following table shows a collection of watchdog meters used to monitor the Orleans watchdog layer. | Meter name | Type | Description | |--|--|--| @@ -212,7 +213,7 @@ The following table represents a collection of watchdog meters that are used to #### Client -The following table represents a collection of client meters that are used to monitor the Orleans client layer. +The following table shows a collection of client meters used to monitor the Orleans client layer. | Meter name | Type | Description | |--|--|--| @@ -220,7 +221,7 @@ The following table represents a collection of client meters that are used to mo #### Miscellaneous -The following table represents a collection of miscellaneous meters that are used to monitor various layers. +The following table shows a collection of miscellaneous meters used to monitor various layers. | Meter name | Type | Description | |--|--|--| @@ -229,7 +230,7 @@ The following table represents a collection of miscellaneous meters that are use #### App requests -The following table represents a collection of app request meters that are used to monitor the Orleans app request layer. +The following table shows a collection of app request meters used to monitor the Orleans app request layer. | Meter name | Type | Description | |--|--|--| @@ -238,7 +239,7 @@ The following table represents a collection of app request meters that are used #### Reminders -The following table represents a collection of reminder meters that are used to monitor the Orleans reminder layer. +The following table shows a collection of reminder meters used to monitor the Orleans reminder layer. | Meter name | Type | Description | |--|--|--| @@ -248,7 +249,7 @@ The following table represents a collection of reminder meters that are used to #### Storage -The following table represents a collection of storage meters that are used to monitor the Orleans storage layer. +The following table shows a collection of storage meters used to monitor the Orleans storage layer. | Meter name | Type | Description | |--|--|--| @@ -261,7 +262,7 @@ The following table represents a collection of storage meters that are used to m #### Streams -The following table represents a collection of stream meters that are used to monitor the Orleans stream layer. +The following table shows a collection of stream meters used to monitor the Orleans stream layer. | Meter name | Type | Description | |--|--|--| @@ -306,7 +307,7 @@ The following table represents a collection of stream meters that are used to mo #### Transactions -The following table represents a collection of transaction meters that are used to monitor the Orleans transaction layer. +The following table shows a collection of transaction meters used to monitor the Orleans transaction layer. | Meter name | Type | Description | |--|--|--| @@ -317,7 +318,7 @@ The following table represents a collection of transaction meters that are used ### Prometheus -There are various third-party metrics providers that you can use with Orleans. One popular example is [Prometheus](https://prometheus.io), which can be used to collect metrics from your app with OpenTelemetry. +Various third-party metrics providers are available for use with Orleans. One popular example is [Prometheus](https://prometheus.io), which you can use to collect metrics from your app with OpenTelemetry. To use OpenTelemetry and Prometheus with Orleans, call the following `IServiceCollection` extension method: @@ -334,9 +335,9 @@ builder.Services.AddOpenTelemetry() > [!IMPORTANT] > Both the [OpenTelemetry.Exporter.Prometheus](https://www.nuget.org/packages/OpenTelemetry.Exporter.Prometheus) and [OpenTelemetry.Exporter.Prometheus.AspNetCore](https://www.nuget.org/packages/OpenTelemetry.Exporter.Prometheus.AspNetCore) NuGet packages are currently in preview as release candidates. They're not recommended for production use. -The `AddPrometheusExporter` method ensures that the `PrometheusExporter` is added to the `builder`. Orleans makes use of a named `"Microsoft.Orleans"` to create instances for many Orleans-specific metrics. The `AddMeter` method is used to specify the name of the meter to subscribe to, in this case `"Microsoft.Orleans"`. +The `AddPrometheusExporter` method ensures the `PrometheusExporter` is added to the `builder`. Orleans uses a named `"Microsoft.Orleans"` to create instances for many Orleans-specific metrics. Use the `AddMeter` method to specify the name of the meter to subscribe to, in this case, `"Microsoft.Orleans"`. -After the exporter has been configured, and your app has been built, you must call `MapPrometheusScrapingEndpoint` on the `IEndpointRouteBuilder` (the `app` instance) to expose the metrics to Prometheus. For example: +After configuring the exporter and building your app, call `MapPrometheusScrapingEndpoint` on the `IEndpointRouteBuilder` (the `app` instance) to expose the metrics to Prometheus. For example: ```csharp WebApplication app = builder.Build(); @@ -347,14 +348,14 @@ app.Run(); ## Distributed tracing -Distributed tracing is a set of tools and practices to monitor and troubleshoot distributed applications. Distributed tracing is a key component of observability, and it's a critical tool for developers to understand the behavior of their apps. Orleans also supports distributed tracing with [OpenTelemetry](https://opentelemetry.io). +Distributed tracing is a set of tools and practices for monitoring and troubleshooting distributed applications. It's a key component of observability and a critical tool for understanding your app's behavior. Orleans supports distributed tracing with [OpenTelemetry](https://opentelemetry.io). -Regardless of the distributed tracing exporter you choose, you call: +Regardless of the distributed tracing exporter you choose, call: - : which enables distributed tracing for the silo. - : which enables distributed tracing for the client. -Referring back to the [Orleans GPS Tracker sample app](/samples/dotnet/samples/orleans-gps-device-tracker-sample), you can use the [Zipkin](https://zipkin.io) distributed tracing system to monitor the app by updating the _Program.cs_. To use OpenTelemetry and Zipkin with Orleans, call the following `IServiceCollection` extension method: +Referring back to the [Orleans GPS Tracker sample app](/samples/dotnet/samples/orleans-gps-device-tracker-sample), you can use the [Zipkin](https://zipkin.io) distributed tracing system to monitor the app by updating _Program.cs_. To use OpenTelemetry and Zipkin with Orleans, call the following `IServiceCollection` extension method: ```csharp builder.Services.AddOpenTelemetry() @@ -378,7 +379,7 @@ builder.Services.AddOpenTelemetry() > [!IMPORTANT] > The [OpenTelemetry.Exporter.Zipkin](https://www.nuget.org/packages/OpenTelemetry.Exporter.Zipkin) NuGet package is currently in preview as a release candidate. It is not recommended for production use. -The Zipkin trace is shown in the Jaeger UI (which is an alternative to Zipkin but uses the same data format): +The Zipkin trace is shown in the Jaeger UI (an alternative to Zipkin that uses the same data format): :::image type="content" source="../media/jaeger-ui.png" lightbox="../media/jaeger-ui.png" alt-text="Orleans GPS Tracker sample app: Jaeger UI trace."::: @@ -390,17 +391,17 @@ For more information, see [Distributed tracing](../../../core/diagnostics/distri :::zone target="docs" pivot="orleans-3-x" -Orleans outputs its runtime statistics and metrics through the interface. The application can register one or more telemetry consumers for their silos and clients, to receive statistics and metrics that the Orleans runtime periodically publishes. These can be consumers for popular telemetry analytics solutions or custom ones for any other destination and purpose. Three telemetry consumers are currently included in the Orleans codebase. +Orleans outputs its runtime statistics and metrics through the interface. Your application can register one or more telemetry consumers for its silos and clients to receive statistics and metrics the Orleans runtime periodically publishes. These can be consumers for popular telemetry analytics solutions or custom ones for any other destination and purpose. Three telemetry consumers are currently included in the Orleans codebase. They're released as separate NuGet packages: -- `Microsoft.Orleans.OrleansTelemetryConsumers.AI` for publishing to [Azure Application Insights](/azure/azure-monitor/app/app-insights-overview). +- `Microsoft.Orleans.OrleansTelemetryConsumers.AI`: For publishing to [Azure Application Insights](/azure/azure-monitor/app/app-insights-overview). -- `Microsoft.Orleans.OrleansTelemetryConsumers.Counters` for publishing to Windows performance counters. The Orleans runtime continually updates many them. The _CounterControl.exe_ tool, included in the [`Microsoft.Orleans.CounterControl`](https://www.nuget.org/packages/Microsoft.Orleans.CounterControl/) NuGet package, helps register necessary performance counter categories. It has to run with elevated privileges. The performance counters can be monitored using any of the standard monitoring tools. +- `Microsoft.Orleans.OrleansTelemetryConsumers.Counters`: For publishing to Windows performance counters. The Orleans runtime continually updates many of them. The _CounterControl.exe_ tool, included in the [`Microsoft.Orleans.CounterControl`](https://www.nuget.org/packages/Microsoft.Orleans.CounterControl/) NuGet package, helps register necessary performance counter categories. It must run with elevated privileges. Monitor the performance counters using any standard monitoring tools. -- `Microsoft.Orleans.OrleansTelemetryConsumers.NewRelic`, for publishing to [New Relic](https://newrelic.com/). +- `Microsoft.Orleans.OrleansTelemetryConsumers.NewRelic`: For publishing to [New Relic](https://newrelic.com/). -To configure your silo and client to use telemetry consumers, silo configuration code looks like this: +To configure your silo and client to use telemetry consumers, the silo configuration code looks like this: ```csharp var siloHostBuilder = new HostBuilder() @@ -410,14 +411,14 @@ var siloHostBuilder = new HostBuilder() }); ``` -Client configuration code look like this: +The client configuration code looks like this: ```csharp var clientBuilder = new ClientBuilder(); clientBuilder.AddApplicationInsightsTelemetryConsumer("INSTRUMENTATION_KEY"); ``` -To use a custom defined (which may have , , and so on), silo configuration code looks like this: +To use a custom-defined (which might have , , etc.), the silo configuration code looks like this: ```csharp var telemetryConfiguration = TelemetryConfiguration.CreateDefault(); diff --git a/docs/orleans/host/monitoring/silo-error-code-monitoring.md b/docs/orleans/host/monitoring/silo-error-code-monitoring.md index 992857a01b8eb..75990199fd1f1 100644 --- a/docs/orleans/host/monitoring/silo-error-code-monitoring.md +++ b/docs/orleans/host/monitoring/silo-error-code-monitoring.md @@ -1,7 +1,7 @@ --- title: Silo error code monitoring description: Explore the various silo error code monitoring values in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 ms.topic: error-reference --- diff --git a/docs/orleans/host/powershell-client.md b/docs/orleans/host/powershell-client.md index 0509e7b89d271..c53f7fb2e3bcf 100644 --- a/docs/orleans/host/powershell-client.md +++ b/docs/orleans/host/powershell-client.md @@ -1,81 +1,82 @@ --- title: PowerShell client module description: Learn about the PowerShell client module in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # PowerShell client module -The Orleans PowerShell client module is a set of [PowerShell Cmdlets](/powershell/scripting/developer/cmdlet/cmdlet-overview) that wraps the . It provides a set of convenient commands, that make it possible to interact with _not_ just the `ManagementGrain` but any `IGrain` just as a regular Orleans application can by using PowerShell scripts. +The Orleans PowerShell client module is a set of [PowerShell Cmdlets](/powershell/scripting/developer/cmdlet/cmdlet-overview) wrapping the . It provides convenient commands allowing interaction not just with the `ManagementGrain` but with any `IGrain`, just as a regular Orleans application can, by using PowerShell scripts. -These cmdlets enable a series of scenarios from start maintenance tasks, tests, monitoring, or any other kind of automation by leveraging PowerShell scripts. +These cmdlets enable various scenarios, from starting maintenance tasks, tests, monitoring, or any other kind of automation by leveraging PowerShell scripts. ## Install the module -To install this module from the source, you can build using the `OrleansPSUtils` project and just import it with: +To install this module from the source, build the `OrleansPSUtils` project and import it using: ```powershell Import-Module .\projectOutputDir\Orleans.psd1 ``` -Although you can do that, there is a much easier and more interesting way by installing it from **PowerShell Gallery**. PowerShell modules are easily shared, much like Nuget packages, but instead of nuget.org, they are hosted on the [PowerShell Gallery](https://www.powershellgallery.com). +Although you can do that, a much easier and more interesting way is to install it from the **PowerShell Gallery**. PowerShell modules are easily shared, much like NuGet packages, but instead of nuget.org, they are hosted on the [PowerShell Gallery](https://www.powershellgallery.com). -To install this module from the PowerShell gallery, run the following command in the desired folder: +To install this module from the PowerShell Gallery, run the following command in the desired folder: ```powershell Save-Module -Name OrleansPSUtils -Path ``` -To install it on your PowerShell modules path (**the recommended way**), run: +To install it in your PowerShell modules path (**the recommended way**), run: ```powershell Install-Module -Name OrleansPSUtils ``` -If you plan to use this module on an [Azure Automation](/azure/automation/overview), [use this link](https://www.powershellgallery.com/packages/Orleans/1.0.0/DeployItemToAzureAutomation?itemType=PSModule&requireLicenseAcceptance=False). +If you plan to use this module in [Azure Automation](/azure/automation/overview), [use this link](https://www.powershellgallery.com/packages/Orleans/1.0.0/DeployItemToAzureAutomation?itemType=PSModule&requireLicenseAcceptance=False). ## Use the module -Regardless of the way you decide to install it, you need to import the module on the current PowerShell session so the cmdlets get available by running this: +Regardless of how you install it, you need to import the module into the current PowerShell session to make the cmdlets available. Run this command: ```powershell Import-Module OrleansPSUtils ``` > [!IMPORTANT] -> In case of building from source, you must import it as suggested on the [Install the module](#install-the-module) section by using the path to the `.psd1` instead of using the module name since it will not be on the `$env:PSModulePath` PowerShell runtime variable. It is highly recommended that you install from PowerShell Gallery instead. +> If building from source, you must import it as suggested in the [Install the module](#install-the-module) section, using the path to the `.psd1` file instead of the module name, since it won't be in the `$env:PSModulePath` PowerShell runtime variable. We highly recommend installing from the PowerShell Gallery instead. -After the module is imported (which means it is loaded on PowerShell session), you will have the following cmdlets available: +After importing the module (loading it into the PowerShell session), the following cmdlets become available: -* `Start-GrainClient` -* `Stop-GrainClient` -* `Get-Grain` +- `Start-GrainClient` +- `Stop-GrainClient` +- `Get-Grain` ### Start the `GrainClient` -This module is a wrapper around and its overloads. +This module wraps and its overloads. #### `Start-GrainClient` -The same as a call to , which will look for the known Orleans client configuration file names: +This is the same as calling , which looks for known Orleans client configuration file names: ```powershell Start-GrainClient [-ConfigFilePath] [[-Timeout] ] ``` -The preceding command will use the provided file path as in `GrainClient.Initialize(filePath)`. +The preceding command uses the provided file path, similar to `GrainClient.Initialize(filePath)`. ```powershell Start-GrainClient [-ConfigFile] [[-Timeout] ] ``` -The preceding command will use an instance of the class representing the config file just as `GrainClient.Initialize(fileInfo)`. +The preceding command uses an instance of the class representing the config file, just like `GrainClient.Initialize(fileInfo)`. ```powershell Start-GrainClient [-Config] [[-Timeout] ] ``` -The preceding command will use an instance of a like in `GrainClient.Initialize(config)`. +The preceding command uses an instance of , similar to `GrainClient.Initialize(config)`. ```powershell Start-GrainClient [-GatewayAddress] [[-OverrideConfig] ] [[-Timeout] ] @@ -84,7 +85,7 @@ Start-GrainClient [-GatewayAddress] [[-OverrideConfig] ] [[-T The preceding command takes an Orleans cluster gateway address endpoint. > [!TIP] -> The `Timeout` parameter is optional and if it is informed and greater than , it will call internally. +> The `Timeout` parameter is optional. If provided and greater than , it internally calls . #### Stop the `GrainClient` @@ -94,13 +95,13 @@ To stop the `GrainClient`, call the following command: Stop-GrainClient ``` -The preceding command takes no parameters and when called, if the `GrainClient` is initialized will be gracefully uninitialized. +The preceding command takes no parameters. When called, if the `GrainClient` is initialized, it will be gracefully uninitialized. #### Get a `Grain` -To get a `Grain`, this cmdlet is a wrapper around `GrainClient.GrainFactory.GetGrain()` and its overloads. The mandatory parameter is `-GrainType` and the `-XXXKey` for the current Grain key types supported by Orleans (`string`, `Guid`, `long`) and also the `-KeyExtension` that can be used on Grains with compound keys. +To get a `Grain`, this cmdlet wraps `GrainClient.GrainFactory.GetGrain()` and its overloads. The mandatory parameter is `-GrainType`. You also need the `-XXXKey` parameter corresponding to the grain key type supported by Orleans (`string`, `Guid`, `long`), and optionally the `-KeyExtension` parameter for grains with compound keys. -This cmdlet returns a grain reference of the type passed by as a parameter on `-GrainType`. As an example on calling `MyInterfacesNamespace.IMyGrain.SayHelloTo` grain method: +This cmdlet returns a grain reference of the type passed via the `-GrainType` parameter. Here's an example of calling the `MyInterfacesNamespace.IMyGrain.SayHelloTo` grain method: ```powershell Import-Module OrleansPSUtils @@ -116,7 +117,7 @@ Hello Gutemberg! Stop-GrainClient ``` -There are additional cmdlets not discussed, but there is support for _Observers_, _Streams_ and other Orleans core features more natively on PowerShell. +There are additional cmdlets not discussed here, but support exists for _Observers_, _Streams_, and other Orleans core features more natively in PowerShell. > [!NOTE] -> The intent is not to reimplement the whole client on PowerShell but instead, give IT and DevOps teams a way to interact with the grains without needing to implement a .NET application. +> The intent isn't to reimplement the entire client in PowerShell but rather to give IT and DevOps teams a way to interact with grains without needing to implement a full .NET application. diff --git a/docs/orleans/host/silo-lifecycle.md b/docs/orleans/host/silo-lifecycle.md index 7ab5a239a39a0..57874e58a2786 100644 --- a/docs/orleans/host/silo-lifecycle.md +++ b/docs/orleans/host/silo-lifecycle.md @@ -1,16 +1,17 @@ --- title: Orleans silo lifecycles description: Learn about .NET Orleans silo lifecycles. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: overview --- # Orleans silo lifecycle overview -Orleans silos use an observable lifecycle for ordered startup and shutdown of Orleans systems, as well as application layer components. For more information on the implementation details, see [Orleans lifecycle](../implementation/orleans-lifecycle.md). +Orleans silos use an observable lifecycle for the ordered startup and shutdown of Orleans systems and application layer components. For more information on the implementation details, see [Orleans lifecycle](../implementation/orleans-lifecycle.md). ## Stages -Orleans silo and cluster clients use a common set of service lifecycle stages. +Orleans silo and cluster clients use a common set of service lifecycle stages: ```csharp public static class ServiceLifecycleStage @@ -28,18 +29,18 @@ public static class ServiceLifecycleStage ``` - : The first stage in the service's lifecycle. -- : The initialization of the runtime environment, where the silo initializes threading. -- : The start of runtime services, where the silo initializes networking and various agents. -- : The initialization of runtime storage. -- : The starting of runtime services for grains. This includes grain type management, membership service, and grain directory. -- : The application layer services. +- : Initialization of the runtime environment, where the silo initializes threading. +- : Start of runtime services, where the silo initializes networking and various agents. +- : Initialization of runtime storage. +- : Starting of runtime services for grains. This includes grain type management, membership service, and grain directory. +- : Application layer services. - : The silo joins the cluster. - : The silo is active in the cluster and ready to accept workload. - : The last stage in the service's lifecycle. ## Logging -Due to the inversion of control, where participants join the lifecycle rather than the lifecycle having some centralized set of initialization steps, it's not always clear from the code what the startup/shutdown order is. To help address this, logging has been added before silo startup to report what components are participating at each stage. These logs are recorded at the _Information_ log level on the logger. For instance: +Due to the inversion of control, where participants join the lifecycle rather than the lifecycle having a centralized set of initialization steps, it's not always clear from the code what the startup/shutdown order is. To help address this, Orleans adds logging before silo startup to report which components participate at each stage. These logs are recorded at the _Information_ log level on the logger. For instance: ```Output Information, Orleans.Runtime.SiloLifecycleSubject, "Stage 2000: Orleans.Statistics.PerfCounterEnvironmentStatistics, Orleans.Runtime.InsideRuntimeClient, Orleans.Runtime.Silo" @@ -49,7 +50,7 @@ Information, Orleans.Runtime.SiloLifecycleSubject, "Stage 4000: Orleans.Runtime. Information, Orleans.Runtime.SiloLifecycleSubject, "Stage 10000: Orleans.Runtime.Versions.GrainVersionStore, Orleans.Storage.AzureTableGrainStorage-Default, Orleans.Storage.AzureTableGrainStorage-PubSubStore" ``` -Additionally, timing and error information are similarly logged for each component by stage. For instance: +Additionally, Orleans similarly logs timing and error information for each component by stage. For instance: ```Output Information, Orleans.Runtime.SiloLifecycleSubject, "Lifecycle observer Orleans.Runtime.InsideRuntimeClient started in stage 2000 which took 33 Milliseconds." @@ -59,7 +60,7 @@ Information, Orleans.Runtime.SiloLifecycleSubject, "Lifecycle observer Orleans.S ## Silo lifecycle participation -Application logic can take part in the silo's lifecycle by registering a participating service in the silo's service container. The service must be registered as an where `T` is a . +Your application logic can participate in the silo's lifecycle by registering a participating service in the silo's service container. Register the service as an , where `T` is . ```csharp public interface ISiloLifecycle : ILifecycleObservable @@ -73,13 +74,13 @@ public interface ILifecycleParticipant } ``` -When the silo starts, all participants (`ILifecycleParticipant`) in the container will be allowed to participate by calling their behavior. Once all have had the opportunity to participate, the silo's observable lifecycle will start all stages in order. +When the silo starts, all participants (`ILifecycleParticipant`) in the container can participate by having their behavior called. Once all have had the opportunity to participate, the silo's observable lifecycle starts all stages in order. ### Example -With the introduction of the silo lifecycle, bootstrap providers, which used to allow application developers to inject logic at the provider initialization phase, are no longer necessary, since application logic can now be injected at any stage of silo startup. Nonetheless, we added a 'startup task' facade to aid the transition for developers who had been using bootstrap providers. As an example of how components can be developed which take part in the silo's lifecycle, we'll look at the startup task facade. +With the introduction of the silo lifecycle, bootstrap providers, which previously allowed you to inject logic at the provider initialization phase, are no longer necessary. You can now inject application logic at any stage of silo startup. Nonetheless, we added a 'startup task' facade to aid the transition for developers who used bootstrap providers. As an example of how you can develop components that participate in the silo's lifecycle, let's look at the startup task facade. -The startup task needs only to inherit from `ILifecycleParticipant` and subscribe the application logic to the silo lifecycle at the specified stage. +The startup task only needs to inherit from `ILifecycleParticipant` and subscribe the application logic to the silo lifecycle at the specified stage. ```csharp class StartupTask : ILifecycleParticipant @@ -107,9 +108,9 @@ class StartupTask : ILifecycleParticipant } ``` -From the above implementation, we can see that in the `Participate(...)` call it subscribes to the silo lifecycle at the configured stage, passing the application callback rather than its initialization logic. Components that need to be initialized at a given stage would provide their callback, but the pattern is the same. Now that we have a `StartupTask` which will ensure that the application's hook is called at the configured stage, we need to ensure that the `StartupTask` participates in the silo lifecycle. +From the preceding implementation, you can see that in the `Participate(...)` call, it subscribes to the silo lifecycle at the configured stage, passing the application callback rather than its initialization logic. Components needing initialization at a given stage would provide their callback, but the pattern remains the same. Now that you have a `StartupTask` ensuring the application's hook is called at the configured stage, you need to ensure the `StartupTask` participates in the silo lifecycle. -For this, we need only register it in the container. We do this with an extension function on the : +For this, you only need to register it in the container. Do this using an extension function on : ```csharp public static ISiloHostBuilder AddStartupTask( @@ -127,4 +128,4 @@ public static ISiloHostBuilder AddStartupTask( } ``` -By registering the StartupTask in the silo's service container as the marker interface `ILifecycleParticipant`, this signals to the silo that this component needs to take part in the silo lifecycle. +By registering the `StartupTask` in the silo's service container as the marker interface `ILifecycleParticipant`, you signal to the silo that this component needs to participate in the silo lifecycle. diff --git a/docs/orleans/implementation/cluster-management.md b/docs/orleans/implementation/cluster-management.md index fac8ed47bd503..1d95d4d270e13 100644 --- a/docs/orleans/implementation/cluster-management.md +++ b/docs/orleans/implementation/cluster-management.md @@ -1,178 +1,185 @@ --- title: Cluster management in Orleans description: Learn about cluster management in .NET Orleans. -ms.date: 11/23/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Cluster management in Orleans -Orleans provides cluster management via a built-in membership protocol, which we sometimes refer to as **Cluster membership**. The goal of this protocol is for all silos (Orleans servers) to agree on the set of currently alive silos, detect failed silos, and allow new silos to join the cluster. +Orleans provides cluster management via a built-in membership protocol, sometimes referred to as **Cluster membership**. The goal of this protocol is for all silos (Orleans servers) to agree on the set of currently alive silos, detect failed silos, and allow new silos to join the cluster. -The protocol relies on an external service to provide an abstraction of . is a flat durable table that we use for two purposes. First, it is used as a rendezvous point for silos to find each other and Orleans clients to find silos. Second, it is used to store the current membership view (list of alive silos) and helps coordinate the agreement on the membership view. +The protocol relies on an external service to provide an abstraction of . `IMembershipTable` is a flat, durable table used for two purposes. First, it serves as a rendezvous point for silos to find each other and for Orleans clients to find silos. Second, it stores the current membership view (list of alive silos) and helps coordinate agreement on this view. -We currently have 6 implementations of the : based on [Azure Table Storage](/azure/storage/storage-dotnet-how-to-use-tables), [Azure Cosmos DB](https://azure.microsoft.com/services/cosmos-db), ADO.NET (PostgreSQL, MySQL/MariaDB, SQL Server, Oracle), [Apache ZooKeeper](https://ZooKeeper.apache.org/), [Consul IO](https://www.consul.io), [AWS DynamoDB](https://aws.amazon.com/dynamodb/), [MongoDB](https://www.mongodb.com/), [Redis](https://redis.io), [Apache Cassandra](https://cassandra.apache.org), and an in-memory implementation for development. +Currently, there are several implementations of `IMembershipTable`: based on [Azure Table Storage](/azure/storage/storage-dotnet-how-to-use-tables), [Azure Cosmos DB](https://azure.microsoft.com/services/cosmos-db), ADO.NET (PostgreSQL, MySQL/MariaDB, SQL Server, Oracle), [Apache ZooKeeper](https://ZooKeeper.apache.org/), [Consul IO](https://www.consul.io), [AWS DynamoDB](https://aws.amazon.com/dynamodb/), [MongoDB](https://www.mongodb.com/), [Redis](https://redis.io), [Apache Cassandra](https://cassandra.apache.org), and an in-memory implementation for development. -In addition to the each silo participates in a fully distributed peer-to-peer membership protocol that detects failed silos and reaches an agreement on a set of alive silos. We describing the internal implementation of Orleans's membership protocol below. +In addition to `IMembershipTable`, each silo participates in a fully distributed peer-to-peer membership protocol that detects failed silos and reaches an agreement on the set of alive silos. The internal implementation of Orleans's membership protocol is described below. ### The membership protocol -1. Upon startup every silo adds an entry for itself into a well-known, shared table, using an implementation of . Orleans uses a combination of silo identity (`ip:port:epoch`) and service deployment ID (cluster ID) as unique keys in the table. Epoch is just time in ticks when this silo started, and as such `ip:port:epoch` is guaranteed to be unique in a given Orleans deployment. +1. Upon startup, every silo adds an entry for itself into a well-known, shared table using an implementation of . Orleans uses a combination of silo identity (`ip:port:epoch`) and service deployment ID (cluster ID) as unique keys in the table. Epoch is simply the time in ticks when this silo started, ensuring `ip:port:epoch` is unique within a given Orleans deployment. -1. Silos monitor each other directly, via application probes ("are you alive" `heartbeats`). probes are sent as direct messages from silo to silo, over the same TCP sockets that silos communicate. That way, probes fully correlate with actual networking problems and server health. Every silo probes a configurable set of other silos. A silo picks whom to probe by calculating consistent hashes on other silos' identity, forming a virtual ring of all identities, and picking X successor silos on the ring (this is a well-known distributed technique called [consistent hashing](https://en.wikipedia.org/wiki/Consistent_hashing) and is widely used in many distributed hash tables, like [Chord DHT](https://en.wikipedia.org/wiki/Chord_(peer-to-peer))). +1. Silos monitor each other directly via application probes ("are you alive" `heartbeats`). Probes are sent as direct messages from silo to silo over the same TCP sockets used for regular communication. This way, probes fully correlate with actual networking problems and server health. Every silo probes a configurable set of other silos. A silo picks whom to probe by calculating consistent hashes on other silos' identities, forming a virtual ring of all identities, and picking X successor silos on the ring. (This is a well-known distributed technique called [consistent hashing](https://en.wikipedia.org/wiki/Consistent_hashing) and is widely used in many distributed hash tables, like [Chord DHT](https://en.wikipedia.org/wiki/Chord_(peer-to-peer))). -1. If a silo S does not get Y probe replies from a monitored server P, it suspects it by writing its timestamped suspicion into P's row in the . +1. If a silo S doesn't receive Y probe replies from a monitored server P, it suspects P by writing its timestamped suspicion into P's row in the `IMembershipTable`. -1. If P has more than Z suspicions within K seconds, then S writes that P is dead into P's row and sends a snapshot of the current membership table to all other silos. Silos refresh the table periodically, so the snapshot is an optimization to reduce the time taken for all silos to learn about the new membership view. +1. If P has more than Z suspicions within K seconds, S writes that P is dead into P's row and broadcasts a snapshot of the current membership table to all other silos. Silos refresh the table periodically, so the snapshot is an optimization to reduce the time it takes for all silos to learn about the new membership view. -1. In more details: +1. In more detail: - 1. Suspicion is written to the , in a special column in the row corresponding to P. When S suspects P it writes: "at time TTT S suspected P". + 1. Suspicion is written to the `IMembershipTable`, in a special column in the row corresponding to P. When S suspects P, it writes: "at time TTT S suspected P". - 1. One suspicion is not enough to declare P as dead. You need Z suspicions from different silos in a configurable time window T, typically 3 minutes, to declare P as dead. The suspicion is written using optimistic concurrency control provided by the . + 1. One suspicion isn't enough to declare P dead. You need Z suspicions from different silos within a configurable time window T (typically 3 minutes) to declare P dead. The suspicion is written using optimistic concurrency control provided by the `IMembershipTable`. - 1. The suspecting silo S reads P's row. + 1. The suspecting silo S reads P's row. - 1. If `S` is the last suspecter (there have already been Z-1 suspecters within a period of T, as written in the suspicion column), S decides to declare P as Dead. In this case, S adds itself to the list of suspecters and also writes in P's Status column that P is Dead. + 1. If `S` is the last suspecter (there have already been Z-1 suspecters within period T, as recorded in the suspicion column), S decides to declare P dead. In this case, S adds itself to the list of suspecters and also writes in P's Status column that P is Dead. - 1. Otherwise, if S is not the last suspecter, S just adds itself to the suspecter's column. + 1. Otherwise, if S isn't the last suspecter, S just adds itself to the suspecter's column. - 1. In either case the write-back uses the version number or ETag that was read, so the updates to this row are serialized. In case the write has failed due to version/ETag mismatch, S retries (read again, and try to write, unless P was already marked dead). + 1. In either case, the write-back uses the version number or ETag read previously, serializing updates to this row. If the write fails due to a version/ETag mismatch, S retries (reads again and tries to write, unless P was already marked dead). - 1. At a high level this sequence of "read, local modify, write back" is a transaction. However, we are not necessarily using storage transactions to do that. "Transaction" code executes locally on a server and we use optimistic concurrency provided by the to ensure isolation and atomicity. + 1. At a high level, this sequence of "read, local modify, write back" is a transaction. However, storage transactions aren't necessarily used. The "transaction" code executes locally on a server, and optimistic concurrency provided by the `IMembershipTable` ensures isolation and atomicity. -1. Every silo periodically reads the entire membership table for its deployment. That way silos learn about new silos joining and about other silos being declared dead. +1. Every silo periodically reads the entire membership table for its deployment. This way, silos learn about new silos joining and about other silos being declared dead. -1. **Snapshot broadcast**: To reduce the frequency of periodical table reads, every time a silo writes to the table (suspicion, new join, etc.) it sends a snapshot of the current table state to all other silos. Since the membership table is consistent and monotonically versioned, each update produces a uniquely versioned snapshot that can be safely shared. This enables immediate propagation of membership changes without waiting for the periodic read cycle. The periodic read is still maintained as a fallback mechanism in case snapshot distribution fails. +1. **Snapshot broadcast**: To reduce the frequency of periodic table reads, every time a silo writes to the table (suspicion, new join, etc.), it sends a snapshot of the current table state to all other silos. Since the membership table is consistent and monotonically versioned, each update produces a uniquely versioned snapshot that can be safely shared. This enables immediate propagation of membership changes without waiting for the periodic read cycle. The periodic read is still maintained as a fallback mechanism in case snapshot distribution fails. -1. **Ordered membership views**: The membership protocol ensures that all membership configurations are globally totally ordered. This ordering provides two key benefits: +1. **Ordered membership views**: The membership protocol ensures all membership configurations are globally totally ordered. This ordering provides two key benefits: - 1. **Guaranteed connectivity**: When a new silo joins the cluster, it must validate two-way connectivity to every other active silo. If any existing silo does not respond (potentially indicating a network connectivity problem), the new silo is not allowed to join. This ensures full connectivity between all silos in the cluster at startup time. See the note about IAmAlive below for an exception in the case of disaster recovery. + 1. **Guaranteed connectivity**: When a new silo joins the cluster, it must validate two-way connectivity to every other active silo. If any existing silo doesn't respond (potentially indicating a network connectivity problem), the new silo isn't allowed to join. This ensures full connectivity between all silos in the cluster at startup time. See the note about `IAmAlive` below for an exception in disaster recovery scenarios. - 2. **Consistent directory updates**: Higher level protocols, such as the distributed grain directory, rely on all silos having a consistent, monotonic view of membership. This enables smarter resolution of duplicate grain activations. For more details, see the [grain directory](grain-directory.md) documentation. + 1. **Consistent directory updates**: Higher-level protocols, such as the distributed grain directory, rely on all silos having a consistent, monotonic view of membership. This enables smarter resolution of duplicate grain activations. For more details, see the [Grain directory](grain-directory.md) documentation. - **Implementation details**: + **Implementation details**: - 1. The requires atomic updates to guarantee a global total order of changes: - - Implementations must update both the table entries (list of silos) and version number atomically - - This can be achieved using database transactions (as in SQL Server) or atomic compare-and-swap operations using ETags (as in Azure Table Storage) - - The specific mechanism depends on the capabilities of the underlying storage system + 1. The `IMembershipTable` requires atomic updates to guarantee a global total order of changes: - 2. A special membership-version row in the table tracks changes: - - Every write to the table (suspicions, death declarations, joins) increments this version number - - All writes are serialized through this row using atomic updates - - The monotonically increasing version ensures a total ordering of all membership changes + - Implementations must update both the table entries (list of silos) and the version number atomically. + - Achieve this using database transactions (as in SQL Server) or atomic compare-and-swap operations using ETags (as in Azure Table Storage). + - The specific mechanism depends on the capabilities of the underlying storage system. - 3. When silo S updates the status of silo P: - - S first reads the latest table state - - In a single atomic operation, it updates both P's row and increments the version number - - If the atomic update fails (for example, due to concurrent modifications), the operation is retried with exponential backoff + 1. A special membership-version row in the table tracks changes: + + - Every write to the table (suspicions, death declarations, joins) increments this version number. + - All writes are serialized through this row using atomic updates. + - The monotonically increasing version ensures a total ordering of all membership changes. + + 1. When silo S updates the status of silo P: + + - S first reads the latest table state. + - In a single atomic operation, it updates both P's row and increments the version number. + - If the atomic update fails (for example, due to concurrent modifications), the operation retries with exponential backoff. **Scalability considerations**: - Serializing all writes through the version row can impact scalability due to increased contention. The protocol has been proven in production with up to 200 silos, but may face challenges beyond a thousand silos. For very large deployments, other parts of Orleans (messaging, grain directory, hosting) remain scalable even if membership updates become a bottleneck. + Serializing all writes through the version row can impact scalability due to increased contention. The protocol has proven effective in production with up to 200 silos but might face challenges beyond a thousand silos. For very large deployments, other parts of Orleans (messaging, grain directory, hosting) remain scalable even if membership updates become a bottleneck. -1. **Default configuration**: The default configuration has been hand-tuned during production usage in Azure. By default: every silo is monitored by three other silos, two suspicions are enough to declare a silo dead, suspicions only from the last three minutes (otherwise they are outdated). probes are sent every ten seconds and you'd need to miss three probes to suspect a silo. +1. **Default configuration**: The default configuration has been hand-tuned during production usage in Azure. By default: every silo is monitored by three other silos, two suspicions are enough to declare a silo dead, and suspicions are only considered from the last three minutes (otherwise they are outdated). Probes are sent every ten seconds, and you need to miss three probes to suspect a silo. 1. **Self-monitoring**: The fault detector incorporates ideas from Hashicorp's _Lifeguard_ research ([paper](https://arxiv.org/abs/1707.00788), [talk](https://www.youtube.com/watch?v=u-a7rVJ6jZY), [blog](https://www.hashicorp.com/blog/making-gossip-more-robust-with-lifeguard)) to improve cluster stability during catastrophic events where a large portion of the cluster experiences partial failure. The `LocalSiloHealthMonitor` component scores each silo's health using multiple heuristics: - * Active status in membership table - * No suspicions from other silos - * Recent successful probe responses - * Recent probe requests received - * Thread pool responsiveness (work items executing within 1 second) - * Timer accuracy (firing within 3 seconds of schedule) + - Active status in the membership table + - No suspicions from other silos + - Recent successful probe responses + - Recent probe requests received + - Thread pool responsiveness (work items executing within 1 second) + - Timer accuracy (firing within 3 seconds of schedule) - A silo's health score affects its probe timeouts: unhealthy silos (scoring 1-8) have increased timeouts compared to healthy silos (score 0). This has two benefits: - * Gives more time for probes to succeed when the network or system is under stress - * Makes it more likely that unhealthy silos will be voted dead before they can incorrectly vote out healthy silos + A silo's health score affects its probe timeouts: unhealthy silos (scoring 1-8) have increased timeouts compared to healthy silos (score 0). This provides two benefits: + + - Gives more time for probes to succeed when the network or system is under stress. + - Makes it more likely that unhealthy silos are voted dead before they can incorrectly vote out healthy silos. This is particularly valuable during scenarios like thread pool starvation, where slow nodes might otherwise incorrectly suspect healthy nodes simply because they cannot process responses quickly enough. -1. **Indirect probing**: Another [Lifeguard](https://arxiv.org/abs/1707.00788)-inspired feature that improves failure detection accuracy by reducing the chance that an unhealthy or partitioned silo will incorrectly declare a healthy silo dead. When a monitoring silo has two probe attempts remaining for a target silo before casting a vote to declare it dead, it employs indirect probing: +1. **Indirect probing**: Another [Lifeguard](https://arxiv.org/abs/1707.00788)-inspired feature improving failure detection accuracy by reducing the chance that an unhealthy or partitioned silo incorrectly declares a healthy silo dead. When a monitoring silo has two probe attempts remaining for a target silo before casting a vote to declare it dead, it employs indirect probing: + + - The monitoring silo randomly selects another silo as an intermediary and asks it to probe the target. + - The intermediary attempts to contact the target silo. + - If the target fails to respond within the timeout period, the intermediary sends a negative acknowledgment. + - If the monitoring silo receives a negative acknowledgment from the intermediary, and the intermediary declares itself healthy (through self-monitoring, described above), the monitoring silo casts a vote to declare the target dead. + - With the default configuration of two required votes, a negative acknowledgment from an indirect probe counts as both votes, allowing faster declaration of dead silos when multiple perspectives confirm the failure. + +1. **Enforcing perfect failure detection**: Once a silo is declared dead in the table, everyone considers it dead, even if it isn't truly dead (e.g., just temporarily partitioned or heartbeat messages were lost). Everyone stops communicating with it. Once the silo learns it's dead (by reading its new status from the table), it terminates its process. Consequently, an infrastructure must be in place to restart the silo as a new process (a new epoch number is generated upon start). When hosted in Azure, this happens automatically. Otherwise, another infrastructure is required, such as a Windows Service configured to auto-restart on failure or a Kubernetes deployment. - * The monitoring silo randomly selects another silo as an intermediary and asks it to probe the target - * The intermediary attempts to contact the target silo - * If the target fails to respond within the timeout period, the intermediary sends a negative acknowledgement - * If the monitoring silo received a negative acknowledgement from the intermediary and the intermediary declares itself healthy (through self-monitoring, described above), the monitoring silo casts a vote to declare the target dead - * With the default configuration of two required votes, a negative acknowledgement from an indirect probe counts as both votes, allowing faster declaration of dead silos when the failure is confirmed by multiple perspectives +1. **What happens if the table is inaccessible for some time**: -1. **Enforcing perfect failure detection**: Once a silo is declared dead in the table, it is considered dead by everyone, even if it is not dead (just partitioned temporarily or heartbeat messages got lost). Everyone stops communicating with it and once it learns that it is dead (by reading its new status from the table) it commits suicide and shuts down its process. As a result, there must be an infrastructure in place to restart the silo as a new process (a new epoch number is generated upon start). When it's hosted in Azure, that happens automatically. When it isn't, another infrastructure is required, such as a Windows Service configured to auto restart on failure or a Kubernetes deployment. + When the storage service is down, unavailable, or experiencing communication problems, the Orleans protocol does NOT mistakenly declare silos dead. Operational silos continue working without issues. However, Orleans won't be able to declare a silo dead (if it detects a dead silo via missed probes, it can't write this fact to the table) and won't be able to allow new silos to join. So, completeness suffers, but accuracy doesn't—partitioning from the table never causes Orleans to mistakenly declare a silo dead. Also, in a partial network partition (where some silos can access the table and others can't), Orleans might declare a silo dead, but it takes time for all other silos to learn about it. Detection might be delayed, but Orleans never wrongly kills a silo due to table unavailability. -1. **What happens if the table is not accessible for some time**: +1. **`IAmAlive` writes for diagnostics and disaster recovery**: - When the storage service is down, unavailable, or there are communication problems with it, the Orleans protocol does NOT declare silos as dead by mistake. Operational silos will keep working without any problems. However, Orleans won't be able to declare a silo dead (if it detects some silo is dead via missed probes, it won't be able to write this fact to the table) and also won't be able to allow new silos to join. So completeness will suffer, but accuracy will not — partitioning from the table will never cause Orleans to declare silo as dead by mistake. Also, in case of a partial network partition (if some silos can access the table and some not), it could happen that Orleans will declare a dead silo as dead, but it will take some time until all other silos learn about it. So detection could be delayed, but Orleans will never wrongly kill a silo due to table unavailability. + In addition to heartbeats sent between silos, each silo periodically updates an "I Am Alive" timestamp in its table row. This serves two purposes: -1. **IAmAlive writes for diagnostics and disaster recovery**: + 1. **Diagnostics**: Provides system administrators a simple way to check cluster liveness and determine when a silo was last active. The timestamp is typically updated every 5 minutes. - In addition to heartbeats that are sent between the silos, each silo periodically updates an "I Am Alive" timestamp in its row in the table. This serves two purposes: - 1. For diagnostics, it provides system administrators with a simple way to check cluster liveness and determine when a silo was last active. The timestamp is typically updated every 5 minutes. - 2. For disaster recovery, if a silo has not updated its timestamp for several periods (configured via `NumMissedTableIAmAliveLimit`), new silos will ignore it during startup connectivity checks, allowing the cluster to recover from scenarios where silos crashed without proper cleanup. + 1. **Disaster recovery**: If a silo hasn't updated its timestamp for several periods (configured via `NumMissedTableIAmAliveLimit`), new silos ignore it during startup connectivity checks. This allows the cluster to recover from scenarios where silos crashed without proper cleanup. ### Membership table -As already mentioned, is used as a rendezvous point for silos to find each other and Orleans clients to find silos and also helps coordinate the agreement on the membership view. The main Orleans repository contains implementations for many systems, such as Azure Table Storage, Azure Cosmos DB, PostgreSQL, MySQL/MariaDB, SQL server, Apache ZooKeeper, Consul IO, Apache Cassandra, MongoDB, Redis, AWS DynamoDB, and an in-memory implementation for development. +As mentioned, `IMembershipTable` serves as a rendezvous point for silos to find each other and for Orleans clients to find silos. It also helps coordinate agreement on the membership view. The main Orleans repository contains implementations for many systems, including Azure Table Storage, Azure Cosmos DB, PostgreSQL, MySQL/MariaDB, SQL Server, Apache ZooKeeper, Consul IO, Apache Cassandra, MongoDB, Redis, AWS DynamoDB, and an in-memory implementation for development. -1. [Azure Table Storage](/azure/storage/storage-dotnet-how-to-use-tables) - in this implementation we use Azure deployment ID as partition key and the silo identity (`ip:port:epoch`) as row key. Together they guarantee a unique key per silo. For concurrency control, we use optimistic concurrency control based on [Azure Table ETags](/rest/api/storageservices/Update-Entity2). Every time we read from the table we store the ETag for every read row and use that ETag when we try to write back. ETags are automatically assigned and checked by the Azure Table service on every write. For multi-row transactions, we utilize the support for [batch transactions provided by Azure table](/rest/api/storageservices/Performing-Entity-Group-Transactions), which guarantees serializable transactions over rows with the same partition key. +1. **[Azure Table Storage](/azure/storage/storage-dotnet-how-to-use-tables)**: In this implementation, the Azure deployment ID serves as the partition key, and the silo identity (`ip:port:epoch`) acts as the row key. Together, they guarantee a unique key per silo. For concurrency control, optimistic concurrency control based on [Azure Table ETags](/rest/api/storageservices/Update-Entity2) is used. Every time data is read from the table, the ETag for each read row is stored and used when trying to write back. The Azure Table service automatically assigns and checks ETags on every write. For multi-row transactions, the support for [batch transactions provided by Azure Table](/rest/api/storageservices/Performing-Entity-Group-Transactions) is utilized, guaranteeing serializable transactions over rows with the same partition key. -1. SQL Server - in this implementation, the configured deployment ID is used to distinguish between deployments and which silos belong to which deployments. The silo identity is defined as a combination of `deploymentID, ip, port, epoch` in appropriate tables and columns. The relational backend uses optimistic concurrency control and transactions, similar to the procedure of using ETags on Azure Table implementation. The relational implementation expects the database engine to generate the ETag used. In the case of SQL Server, on SQL Server 2000 the generated ETag is one acquired from a call to `NEWID()`. On SQL Server 2005 and later [ROWVERSION](/sql/t-sql/data-types/rowversion-transact-sql) is used. Orleans reads and writes relational ETags as opaque `VARBINARY(16)` tags and stores them in memory as [base64]( https://en.wikipedia.org/wiki/Base64) encoded strings. Orleans supports multi-row inserts using `UNION ALL` (for Oracle including DUAL), which is currently used to insert statistics data. The exact implementation and rationale for SQL Server can be seen at [CreateOrleansTables_SqlServer.sql](https://github.com/dotnet/orleans/blob/ba30bbb2155168fc4b9f190727220583b9a7ae4c/src/OrleansSQLUtils/CreateOrleansTables_SqlServer.sql). +1. **SQL Server**: In this implementation, the configured deployment ID distinguishes between deployments and which silos belong to which deployments. The silo identity is defined as a combination of `deploymentID, ip, port, epoch` in appropriate tables and columns. The relational backend uses optimistic concurrency control and transactions, similar to using ETags in the Azure Table implementation. The relational implementation expects the database engine to generate the ETag. For SQL Server 2000, the generated ETag is acquired from a call to `NEWID()`. On SQL Server 2005 and later, [ROWVERSION](/sql/t-sql/data-types/rowversion-transact-sql) is used. Orleans reads and writes relational ETags as opaque `VARBINARY(16)` tags and stores them in memory as [base64](https://en.wikipedia.org/wiki/Base64) encoded strings. Orleans supports multi-row inserts using `UNION ALL` (for Oracle, including `DUAL`), which is currently used to insert statistics data. The exact implementation and rationale for SQL Server is available at [CreateOrleansTables_SqlServer.sql](https://github.com/dotnet/orleans/blob/ba30bbb2155168fc4b9f190727220583b9a7ae4c/src/OrleansSQLUtils/CreateOrleansTables_SqlServer.sql). -1. [Apache ZooKeeper](https://zookeeper.apache.org/) - in this implementation we use the configured deployment ID as a root node and the silo identity (`ip:port@epoch`) as its child node. Together they guarantee a unique path per silo. For concurrency control, we use optimistic concurrency control based on the [node version](https://zookeeper.apache.org/doc/r3.4.6/zookeeperOver.html#Nodes+and+ephemeral+nodes). Every time we read from the deployment root node we store the version for every read child silo node and use that version when we try to write back. Each time a node's data changes, the version number increases atomically by the ZooKeeper service. For multi-row transactions, we utilize the [multi method](https://zookeeper.apache.org/doc/r3.4.6/api/org/apache/zookeeper/ZooKeeper.html#multi(java.lang.Iterable)), which guarantees serializable transactions over silo nodes with the same parent deployment ID node. +1. **[Apache ZooKeeper](https://zookeeper.apache.org/)**: In this implementation, the configured deployment ID is used as a root node, and the silo identity (`ip:port@epoch`) as its child node. Together, they guarantee a unique path per silo. For concurrency control, optimistic concurrency control based on the [node version](https://zookeeper.apache.org/doc/r3.4.6/zookeeperOver.html#Nodes+and+ephemeral+nodes) is used. Every time data is read from the deployment root node, the version for every read child silo node is stored and used when trying to write back. Each time a node's data changes, the ZooKeeper service atomically increases the version number. For multi-row transactions, the [multi method](https://zookeeper.apache.org/doc/r3.4.6/api/org/apache/zookeeper/ZooKeeper.html#multi(java.lang.Iterable)) is utilized, guaranteeing serializable transactions over silo nodes with the same parent deployment ID node. -1. [Consul IO](https://www.consul.io) - we used [Consul's Key/Value store](https://www.consul.io/intro/getting-started/kv.html) to implement the membership table. Refer to [Consul-Deployment](../deployment/consul-deployment.md) for more details. +1. **[Consul IO](https://www.consul.io)**: Consul's Key/Value store was used to implement the membership table. See [Consul Deployment](../deployment/consul-deployment.md) for more details. -1. [AWS DynamoDB](https://aws.amazon.com/dynamodb/) - In this implementation, we use the cluster Deployment ID as the Partition Key and Silo Identity (`ip-port-generation`) as the RangeKey making the record unity. The optimistic concurrency is made by the `ETag` attribute by making conditional writes on DynamoDB. The implementation logic is quite similar to Azure Table Storage. +1. **[AWS DynamoDB](https://aws.amazon.com/dynamodb/)**: In this implementation, the cluster Deployment ID is used as the Partition Key and Silo Identity (`ip-port-generation`) as the RangeKey, making the record unique. Optimistic concurrency is achieved using the `ETag` attribute by making conditional writes on DynamoDB. The implementation logic is quite similar to Azure Table Storage. -1. [Apacha Cassandra](https://cassandra.apache.org/_/index.html) - In this implementation we use the composite of Service Id and Cluster Id as partition key and the silo identity (`ip:port:epoch`) as row key. Together they guarantee a unique row per silo. For concurrency control, we use optimistic concurrency control based on a static column version using a Lightweight Transaction. This version column is shared for all rows in the partition/cluster so provides the consistent incrementing version number for each cluster's membership table. There are no multi-row transactions in this implementation. +1. **[Apache Cassandra](https://cassandra.apache.org/_/index.html)**: In this implementation, the composite of Service ID and Cluster ID serves as the partition key, and the silo identity (`ip:port:epoch`) as the row key. Together, they guarantee a unique row per silo. For concurrency control, optimistic concurrency control based on a static column version using a Lightweight Transaction is used. This version column is shared for all rows in the partition/cluster, providing a consistent incrementing version number for each cluster's membership table. There are no multi-row transactions in this implementation. -1. In-memory emulation for development setup. We use a special system grain for that implementation. This grain lives on a designated primary silo, which is only used for a **development setup**. In any real production usage primary silo **is not required**. +1. **In-memory emulation for development setup**: A special system grain is used for this implementation. This grain lives on a designated primary silo, which is only used for a **development setup**. In any real production usage, a primary silo **isn't required**. ### Design rationale -A natural question that might be asked is why not rely completely on [Apache ZooKeeper](https://ZooKeeper.apache.org/) or [etcd](https://etcd.io/) for the cluster membership implementation, potentially by using ZooKeeper's out-of-the-box support for group membership with ephemeral nodes? Why did we bother implementing our membership protocol? There were primarily three reasons: +A natural question might be why not rely completely on [Apache ZooKeeper](https://ZooKeeper.apache.org/) or [etcd](https://etcd.io/) for the cluster membership implementation, potentially using ZooKeeper's out-of-the-box support for group membership with ephemeral nodes? Why implement our membership protocol? There were primarily three reasons: 1. **Deployment/Hosting in the cloud**: - Zookeeper is not a hosted service. It means that in the Cloud environment Orleans customers would have to deploy/run/manage their instance of a ZK cluster. This is just yet another unnecessary burden, that we did not want to force on our customers. By using Azure Table we rely on a hosted, managed service which makes our customer's lives much simpler. _Basically, in the Cloud, use Cloud as a Platform, not as an Infrastructure._ On the other hand, when running on-premises and managing your servers, relying on ZK as an implementation of the is a viable option. + Zookeeper isn't a hosted service. This means in a Cloud environment, Orleans customers would have to deploy, run, and manage their instance of a ZK cluster. This is an unnecessary burden that wasn't forced on customers. By using Azure Table, Orleans relies on a hosted, managed service, making customers' lives much simpler. _Basically, in the Cloud, use Cloud as a Platform, not Infrastructure._ On the other hand, when running on-premises and managing your servers, relying on ZK as an implementation of `IMembershipTable` is a viable option. 1. **Direct failure detection**: - When using ZK's group membership with ephemeral nodes the failure detection is performed between the Orleans servers (ZK clients) and ZK servers. This may not necessarily correlate with the actual network problems between Orleans servers. _Our desire was that the failure detection would accurately reflect the intra-cluster state of the communication._ Specifically, in our design, if an Orleans silo cannot communicate with the it is not considered dead and can keep working. As opposed to that, have we used ZK group membership with ephemeral nodes a disconnection from a ZK server may cause an Orleans silo (ZK client) to be declared dead, while it may be alive and fully functional. + When using ZK's group membership with ephemeral nodes, failure detection occurs between the Orleans servers (ZK clients) and ZK servers. This might not necessarily correlate with actual network problems between Orleans servers. _The desire was that failure detection accurately reflects the intra-cluster state of communication._ Specifically, in this design, if an Orleans silo can't communicate with `IMembershipTable`, it isn't considered dead and can continue working. In contrast, if ZK group membership with ephemeral nodes were used, a disconnection from a ZK server might cause an Orleans silo (ZK client) to be declared dead, while it might be alive and fully functional. 1. **Portability and flexibility**: - As part of Orleans's philosophy, we do not want to force a strong dependence on any particular technology, but rather have a flexible design where different components can be easily switched with different implementations. This is exactly the purpose that abstraction serves. + As part of Orleans's philosophy, Orleans doesn't force a strong dependence on any particular technology but rather provides a flexible design where different components can be easily switched with different implementations. This is exactly the purpose the `IMembershipTable` abstraction serves. ### Properties of the membership protocol 1. **Can handle any number of failures**: - Our algorithm can handle any number of failures (that is, f<=n), including full cluster restart. This is in contrast with "traditional" [Paxos](https://en.wikipedia.org/wiki/Paxos_(computer_science)) based solutions, which require a quorum, which is usually a majority. We have seen in production situations when more than half of the silos were down. Our system stayed functional, while Paxos-based membership would not be able to make progress. + This algorithm can handle any number of failures (f<=n), including full cluster restart. This contrasts with "traditional" [Paxos](https://en.wikipedia.org/wiki/Paxos_(computer_science))-based solutions, which require a quorum (usually a majority). Production situations have shown scenarios where more than half of the silos were down. This system stayed functional, while Paxos-based membership wouldn't be able to make progress. 1. **Traffic to the table is very light**: - The actual probes go directly between servers and not to the table. This would generate a lot of traffic plus would be less accurate from the failure detection perspective - if a silo could not reach the table, it would miss writing its I am alive heartbeat, and others would kill him. + Actual probes go directly between servers, not to the table. Routing probes through the table would generate significant traffic and be less accurate from a failure detection perspective – if a silo couldn't reach the table, it would miss writing its "I am alive" heartbeat, and others would declare it dead. 1. **Tunable accuracy versus completeness**: - While you cannot achieve both perfect and accurate failure detection, one usually wants an ability to tradeoff accuracy (don't want to declare a silo that is alive as dead) with completeness (want to declare dead a silo that is indeed dead as soon as possible). The configurable votes to declare dead and missed probes allow trading those two. For more information, see [Yale University: Computer Science Failure Detectors](https://www.cs.yale.edu/homes/aspnes/pinewiki/FailureDetectors.html). + While you can't achieve both perfect and accurate failure detection, you usually want the ability to trade off accuracy (not wanting to declare a live silo dead) with completeness (wanting to declare a dead silo dead as soon as possible). The configurable votes to declare dead and missed probes allow trading these two aspects. For more information, see [Yale University: Computer Science Failure Detectors](https://www.cs.yale.edu/homes/aspnes/pinewiki/FailureDetectors.html). 1. **Scale**: - The protocol can handle thousands and probably even tens of thousands of servers. This is in contrast with traditional Paxos-based solutions, such as group communication protocols, which are known not to scale beyond tens. + The protocol can handle thousands, probably even tens of thousands, of servers. This contrasts with traditional Paxos-based solutions, such as group communication protocols, which are known not to scale beyond tens of nodes. 1. **Diagnostics**: - The table is also very convenient for diagnostics and troubleshooting. The system administrators can instantaneously find in the table the current list of alive silos, as well as see the history of all killed silos and suspicions. This is especially useful when diagnosing problems. + The table is also very convenient for diagnostics and troubleshooting. System administrators can instantaneously find the current list of alive silos in the table, as well as see the history of all killed silos and suspicions. This is especially useful when diagnosing problems. -1. **Why do we need reliable persistent storage for implementation of the **: +1. **Why is reliable persistent storage needed for the implementation of `IMembershipTable`**: - We use persistent storage for the for two purposes. First, it is used as a rendezvous point for silos to find each other and Orleans clients to find silos. Second, we use reliable storage to help us coordinate the agreement on the membership view. While we perform failure detection directly in a peer-to-peer fashion between the silos, we store the membership view in reliable storage and use the concurrency control mechanism provided by this storage to reach an agreement of who is alive and who is dead. That way, in a sense, our protocol outsources the hard problem of distributed consensus to the cloud. In that we fully utilize the power of the underlying cloud platform, using it truly as Platform as a Service (PaaS). + Persistent storage is used for `IMembershipTable` for two purposes. First, it serves as a rendezvous point for silos to find each other and for Orleans clients to find silos. Second, reliable storage helps coordinate agreement on the membership view. While failure detection occurs directly peer-to-peer between silos, the membership view is stored in reliable storage, and the concurrency control mechanism provided by this storage is used to reach an agreement on who is alive and who is dead. In a sense, this protocol outsources the hard problem of distributed consensus to the cloud. In doing so, the full power of the underlying cloud platform is utilized, using it truly as Platform as a Service (PaaS). -1. **Direct IAmAlive writes into the table for diagnostics only**: +1. **Direct `IAmAlive` writes into the table for diagnostics only**: - In addition to heartbeats that are sent between the silos, each silo also periodically updates an "I Am Alive" column in his row in the table. This "I Am Alive" column is only used **for manual troubleshooting and diagnostics** and is not used by the membership protocol itself. It is usually written at a much lower frequency (once every 5 minutes) and serves as a very useful tool for system administrators to check the liveness of the cluster or easily find out when the silo was last alive. + In addition to heartbeats sent between silos, each silo also periodically updates an "I Am Alive" column in its table row. This "I Am Alive" column is only used **for manual troubleshooting and diagnostics** and isn't used by the membership protocol itself. It's usually written at a much lower frequency (once every 5 minutes) and serves as a very useful tool for system administrators to check the liveness of the cluster or easily find out when the silo was last alive. ### Acknowledgements -We would like to acknowledge the contribution of Alex Kogan to the design and implementation of the first version of this protocol. This work was done as part of a summer internship in Microsoft Research in the Summer of 2011. -The implementation of ZooKeeper based was done by [Shay Hazor](https://github.com/shayhatsor), the implementation of SQL was done by [Veikko Eeva](https://github.com/veikkoeeva), the implementation of AWS DynamoDB was done by [Gutemberg Ribeiro](https://github.com/galvesribeiro/) and the implementation of Consul based was done by [Paul North](https://github.com/PaulNorth), and finally the implementation of the Apache Cassandra was adapted from `OrleansCassandraUtils` by [Arshia001](https://github.com/Arshia001). +Acknowledgments for the contribution of Alex Kogan to the design and implementation of the first version of this protocol. This work was done as part of a summer internship in Microsoft Research in the Summer of 2011. +The implementation of ZooKeeper based `IMembershipTable` was done by [Shay Hazor](https://github.com/shayhatsor), the implementation of SQL `IMembershipTable` was done by [Veikko Eeva](https://github.com/veikkoeeva), the implementation of AWS DynamoDB `IMembershipTable` was done by [Gutemberg Ribeiro](https://github.com/galvesribeiro/), the implementation of Consul based `IMembershipTable` was done by [Paul North](https://github.com/PaulNorth), and finally the implementation of the Apache Cassandra `IMembershipTable` was adapted from `OrleansCassandraUtils` by [Arshia001](https://github.com/Arshia001). diff --git a/docs/orleans/implementation/grain-directory.md b/docs/orleans/implementation/grain-directory.md index 36613770d25bb..669d856a07f1b 100644 --- a/docs/orleans/implementation/grain-directory.md +++ b/docs/orleans/implementation/grain-directory.md @@ -1,20 +1,21 @@ --- title: Grain Directory Implementation description: Explore the implementation of the grain directory in .NET Orleans. -ms.date: 11/22/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Grain directory implementation ## Overview and architecture -The grain directory in Orleans is a key-value store where the key is a grain identifier and the value is a registration entry which points to an active silo which (potentially) hosts the grain. +The grain directory in Orleans is a key-value store where the key is a grain identifier and the value is a registration entry pointing to an active silo that (potentially) hosts the grain. -While Orleans provides a default in-memory distributed directory implementation (described in this article), the grain directory system is designed to be pluggable. Developers can implement their own directory by implementing the `IGrainDirectory` interface and registering it with the silo's service collection. This allows for custom directory implementations that might use different storage backends or consistency models to better suit specific application requirements. Since the introduction of the new strong consistency directory, there is little need for external directory implementations, but the API remains for backward compatibility and flexibility. The grain directory can be configured on a per-grain-type basis. +While Orleans provides a default in-memory distributed directory implementation (described in this article), the grain directory system is designed to be pluggable. You can implement your own directory by implementing the `IGrainDirectory` interface and registering it with the silo's service collection. This allows for custom directory implementations that might use different storage backends or consistency models to better suit specific application requirements. Since the introduction of the new strong consistency directory, there's less need for external directory implementations, but the API remains for backward compatibility and flexibility. You can configure the grain directory on a per-grain-type basis. -To optimize performance, directory lookups are cached locally within each silo. This means that potentially-remote directory reads are only necessary when the local cache entry is either missing or invalid. This caching mechanism reduces the network overhead and latency associated with grain location lookups. +To optimize performance, Orleans caches directory lookups locally within each silo. This means potentially remote directory reads are only necessary when the local cache entry is missing or invalid. This caching mechanism reduces network overhead and latency associated with grain location lookups. -Originally, Orleans implemented an eventually consistent directory structured as a distributed hash table. This was superseded by a strongly consistent directory in Orleans v9.0, based on the two-phase [Virtually Synchronous methodology](https://www.microsoft.com/en-us/research/publication/virtually-synchronous-methodology-for-dynamic-service-replication/) and also structured as distributed hash table but with improved load balancing through the use of virtual nodes. This article describes the latter, newer grain directory implementation. +Originally, Orleans implemented an eventually consistent directory structured as a distributed hash table. This was superseded by a strongly consistent directory in Orleans v9.0, based on the two-phase [Virtually Synchronous methodology](https://www.microsoft.com/en-us/research/publication/virtually-synchronous-methodology-for-dynamic-service-replication/). It's also structured as a distributed hash table but offers improved load balancing through virtual nodes. This article describes the latter, newer grain directory implementation. ## Distributed grain directory @@ -22,8 +23,8 @@ The distributed grain directory in Orleans offers strong consistency, even load Directory partitions have two modes of operation: -1. Normal operation: partitions process requests locally without coordination with other hosts. -1. View change: hosts coordinate with each other to transfer ownership of directory ranges. +1. **Normal operation**: Partitions process requests locally without coordination with other hosts. +1. **View change**: Hosts coordinate with each other to transfer ownership of directory ranges. The directory leverages Orleans' strong consistency cluster membership system, where configurations called "views" have monotonically increasing version numbers. As silos join and leave the cluster, successive views are created, resulting in changes to range ownership. @@ -32,29 +33,28 @@ All directory operations include view coordination: - Requests carry the caller's view number. - Responses include the partition's view number. - View number mismatches trigger synchronization. -- Requests are automatically retried on view changes. +- Requests automatically retry on view changes. -This ensures that all requests are processed by the correct owner of the directory partition. +This ensures the correct owner of the directory partition processes all requests. ### Partitioning strategy -The directory is partitioned using a consistent hash ring with ranges being assigned to the active silos in the cluster. Grain identifiers are hashed to find the silo which owns the section of the ring corresponding to its hash. +The directory is partitioned using a consistent hash ring, with ranges assigned to the active silos in the cluster. Grain identifiers are hashed to find the silo owning the section of the ring corresponding to its hash. -Each active silo owns a pre-configured number of ranges, defaulting to 30 ranges per silo. This is similar to the scheme used by [Amazon Dynamo](https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf) and [Apache Cassandra](https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/architecture/archDataDistributeVnodesUsing.html), where multiple "virtual nodes" (ranges) are created for each node (host). +Each active silo owns a pre-configured number of ranges, defaulting to 30 ranges per silo. This is similar to the scheme used by [Amazon Dynamo](https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf) and [Apache Cassandra](https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/architecture/archDataDistributeVnodesUsing.html), where multiple "virtual nodes" (ranges) are created for each physical node (host). -The size of a partition is determined by the distance between its hash and the hash of the next partition. It's possible for a range to be split among multiple silos during a view change, which adds complexity to the view change procedure since each partition must potentially coordinate with multiple other partitions. +The size of a partition is determined by the distance between its hash and the hash of the next partition. It's possible for a range to be split among multiple silos during a view change. This adds complexity to the view change procedure, as each partition must potentially coordinate with multiple other partitions. ### View change procedure -Directory partitions (implemented in `GrainDirectoryPartition`) use versioned range locks to prevent invalid access to ranges during view changes. Range locks are created during view change and are released when the view change is complete. These locks are analogous to the 'wedges' used in the Virtual Synchrony methodology. +Directory partitions (implemented in `GrainDirectoryPartition`) use versioned range locks to prevent invalid access to ranges during view changes. Range locks are created during a view change and released when the view change completes. These locks are analogous to the 'wedges' used in the Virtual Synchrony methodology. When a view change occurs, a partition can either grow or shrink: -- If a new silo has joined the cluster, then existing partitions may shrink to make room. -- If a silo has left the cluster, then remaining partitions may grow to take over the orphaned ranges. +- If a new silo joins the cluster, existing partitions might shrink to make room. +- If a silo leaves the cluster, remaining partitions might grow to take over the orphaned ranges. -Directory registrations must be transferred from the old owner to the new owner before requests can be served. -The transfer process follows these steps: +Directory registrations must transfer from the old owner to the new owner before requests can be served. The transfer process follows these steps: 1. The previous owner seals the range and creates a snapshot of its directory entries. 1. The new owner requests and applies the snapshot. @@ -69,10 +69,10 @@ When a host crashes without properly handing off its directory partitions, the s 1. Rebuilding the directory state for affected ranges. 1. Ensuring no duplicate grain activations occur. -Recovery is also necessary when cluster membership changes happen rapidly. While cluster membership guarantees monotonicity, it's possible for silos to miss intermediate membership views. In such cases: +Recovery is also necessary when cluster membership changes rapidly. While cluster membership guarantees monotonicity, it's possible for silos to miss intermediate membership views. In such cases: - Snapshot transfers are abandoned. -- Recovery is performed instead of normal partition-to-partition handover. +- Recovery is performed instead of the normal partition-to-partition handover. - The system maintains consistency despite missing intermediate states. -A future improvement to cluster membership may reduce or eliminate these scenarios by ensuring all views are seen by all silos. +A future improvement to cluster membership might reduce or eliminate these scenarios by ensuring all silos see all views. diff --git a/docs/orleans/implementation/index.md b/docs/orleans/implementation/index.md index f7ad2b2f79e04..8ffd8bd1aa0e4 100644 --- a/docs/orleans/implementation/index.md +++ b/docs/orleans/implementation/index.md @@ -1,34 +1,31 @@ --- title: Implementation details description: Explore the various implementation details in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: overview --- # Implementation details overview -## [Orleans Lifecycle](orleans-lifecycle.md) +## [Orleans lifecycle](orleans-lifecycle.md) -Some Orleans behaviors are sufficiently complex that they need ordered startup and shutdown. -To address this, Orleans introduced a general component lifecycle pattern. +Some Orleans behaviors are sufficiently complex that they need ordered startup and shutdown. To address this, Orleans introduced a general component lifecycle pattern. ## [Messaging delivery guarantees](messaging-delivery-guarantees.md) -Orleans messaging delivery guarantees are **at-most-once**, by default. -Optionally, if configured to do retries upon timeout, Orleans provides at-least-once delivery instead. +Orleans messaging delivery guarantees are **at-most-once** by default. Optionally, if you configure retries upon timeout, Orleans provides at-least-once delivery instead. ## [Scheduler](scheduler.md) -Orleans Scheduler is a component within the Orleans runtime responsible for executing application code and parts of the runtime code to ensure the single-threaded execution semantics. +The Orleans Scheduler is a component within the Orleans runtime responsible for executing application code and parts of the runtime code to ensure single-threaded execution semantics. ## [Cluster management](cluster-management.md) -Orleans provides cluster management via a built-in membership protocol, which we sometimes refer to as Silo Membership. -The goal of this protocol is for all silos (Orleans servers) to agree on the set of currently alive silos, detect failed silos, and allow new silos to join the cluster. +Orleans provides cluster management via a built-in membership protocol, sometimes referred to as Silo Membership. The goal of this protocol is for all silos (Orleans servers) to agree on the set of currently alive silos, detect failed silos, and allow new silos to join the cluster. ## [Streams implementation](streams-implementation/index.md) -This section provides a high-level overview of Orleans Stream implementation. -It describes concepts and details that are not visible on the application level. +This section provides a high-level overview of the Orleans Stream implementation. It describes concepts and details not visible at the application level. ## [Load balancing](load-balancing.md) @@ -36,4 +33,4 @@ Load balancing, in a broad sense, is one of the pillars of the Orleans runtime. ## [Unit testing](testing.md) -This section shows how to unit test your grains to make sure they behave correctly. +This section shows how to unit test your grains to ensure they behave correctly. diff --git a/docs/orleans/implementation/load-balancing.md b/docs/orleans/implementation/load-balancing.md index f52ef1f8d48e1..e19970df48feb 100644 --- a/docs/orleans/implementation/load-balancing.md +++ b/docs/orleans/implementation/load-balancing.md @@ -1,19 +1,20 @@ --- title: Load balancing description: Learn how .NET Orleans manages load balancing. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Load balancing -Load balancing, in a broad sense, is one of the pillars of the Orleans runtime. Orleans runtime tries to make everything balanced since balancing allows to maximize resource usage and avoid hotspots, which leads to better performance, as well as helps with elasticity. Load balancing in Orleans applies in multiple places. Below is a non-exhaustive list of places where the runtime performs balancing: +Load balancing, in a broad sense, is one of the pillars of the Orleans runtime. The Orleans runtime tries to keep everything balanced, as balancing maximizes resource usage, avoids hotspots, leads to better performance, and helps with elasticity. Load balancing in Orleans applies in multiple places. Below is a non-exhaustive list of where the runtime performs balancing: -1. The default actor placement strategy is random, new activations are placed randomly across silos. That results in a balanced placement and prevents hotspots for most scenarios. -1. A more advanced tries to equalize the number of activations on all silos, which results in a more even distribution of activations across silos. This is especially important for elasticity. -1. The grain directory service is built on top of a distributed hash table, which inherently is balanced. The directory service maps grains to activations, each silo owns part of the global mapping table, and this table is globally partitioned in a balanced way across all silos. We use consistent hashing with virtual buckets for that. -1. Clients connect to all gateways and spread their requests across them, in a balanced way. -1. The reminder service is a distributed partitioned runtime service. The assignment of which silo is responsible to serve which reminder is balanced across all silos via consistent hashing, just like in grain directory. -1. Performance critical components within a silo are partitioned, and the work across them is locally balanced. That way the silo runtime can fully utilize all available CPU cores and not create in-silo bottlenecks. This applies to all local resources: allocation of work to threads, sockets, dispatch responsibilities, queues, etc. +1. The default actor placement strategy is random; new activations are placed randomly across silos. This results in balanced placement and prevents hotspots for most scenarios. +1. A more advanced strategy tries to equalize the number of activations on all silos, resulting in a more even distribution across silos. This is especially important for elasticity. +1. The grain directory service builds on top of a distributed hash table, which is inherently balanced. The directory service maps grains to activations. Each silo owns part of the global mapping table, and this table is globally partitioned in a balanced way across all silos using consistent hashing with virtual buckets. +1. Clients connect to all gateways and spread their requests across them in a balanced way. +1. The reminder service is a distributed, partitioned runtime service. The assignment of which silo is responsible for serving which reminder is balanced across all silos via consistent hashing, just like the grain directory. +1. Performance-critical components within a silo are partitioned, and the work across them is locally balanced. This allows the silo runtime to fully utilize all available CPU cores and avoid in-silo bottlenecks. This applies to all local resources: allocation of work to threads, sockets, dispatch responsibilities, queues, etc. 1. The balances the responsibility of pulling events from persistence queues across silos in the cluster. -Balancing doesn't necessarily mean loss of locality. One can be balanced and still maintain a good locality. For example, when balancing means sharding/partitioning, you can partition responsibility for a certain logical task, while still maintaining locality within each partition. That applies both for local and distributed balancing. +Balancing doesn't necessarily mean loss of locality. You can achieve balance while still maintaining good locality. For example, when balancing involves sharding/partitioning, you can partition responsibility for a certain logical task while maintaining locality within each partition. This applies to both local and distributed balancing. diff --git a/docs/orleans/implementation/messaging-delivery-guarantees.md b/docs/orleans/implementation/messaging-delivery-guarantees.md index dcc912824616f..35edac8e96297 100644 --- a/docs/orleans/implementation/messaging-delivery-guarantees.md +++ b/docs/orleans/implementation/messaging-delivery-guarantees.md @@ -1,26 +1,27 @@ --- title: Messaging delivery guarantees description: Learn about messaging delivery guarantees in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Messaging delivery guarantees -Orleans messaging delivery guarantees are **at-most-once**, by default. Optionally, if configured to do retries upon timeout, Orleans provides at-least-once deliv­ery instead. +Orleans messaging delivery guarantees are **at-most-once** by default. Optionally, if you configure retries upon timeout, Orleans provides at-least-once delivery instead. In more detail: -* Every message in Orleans has an automatic timeout (the exact timeout can be configured). If the reply does not arrive on time, the returned is broken with a timeout exception. -* Orleans can be configured to do automatic retries upon timeout. By default, it _does not_ do automatic retries. -* Application code of course can also choose to do retries upon timeout. +- Every message in Orleans has an automatic timeout (you can configure the exact timeout). If the reply doesn't arrive on time, the returned breaks with a timeout exception. +- You can configure Orleans to perform automatic retries upon timeout. By default, it _does not_ perform automatic retries. +- Your application code can also choose to implement retries upon timeout. -If the Orleans system is configured not to do automatic retries (default setting) and the application is not resending – **Orleans provides at-most-once message delivery**. A message will either be delivered once or not at all. **It will never be delivered twice.** +If the Orleans system is configured not to perform automatic retries (the default setting) and your application doesn't resend messages, **Orleans provides at-most-once message delivery**. A message is either delivered once or not at all. **It's never delivered twice.** -In the system with retries (either by the runtime or by the application), the message may arrive multiple times. Orleans currently does nothing to durably store which messages already arrived and suppress the second delivery. (We believe this would be pretty costly.) So in a system with retries Orleans does NOT guarantee at most once delivery. +In a system with retries (either by the runtime or by the application), the message might arrive multiple times. Orleans currently doesn't durably store which messages have already arrived or suppress subsequent deliveries. (We believe this would be quite costly.) So, in a system with retries, Orleans does NOT guarantee at-most-once delivery. -**If you keep retrying potentially indefinitely**, **the message will eventually arrive**, thus providing the at-least-once delivery guarantee. Notice that "will eventually arrive" is something that the runtime needs to guarantee. It does not come for free just by itself even if you keep retrying. Orleans provides eventual delivery since grains never go into any permanent failure state and a failed grain will eventually be re-activated on another silo. +**If you keep retrying potentially indefinitely**, **the message eventually arrives**, thus providing the at-least-once delivery guarantee. Note that "eventually arrives" is something the runtime needs to guarantee. It doesn't happen automatically just because you keep retrying. Orleans provides eventual delivery because grains never enter a permanent failure state, and a failed grain eventually reactivates on another silo. -**So to summarize**: in the system without retries Orleans guarantees at-most-once message delivery. In the system with infinite retries, Orleans guarantees at-least-once (and _does not_ guarantee at-most-once). +**To summarize**: In a system without retries, Orleans guarantees at-most-once message delivery. In a system with infinite retries, Orleans guarantees at-least-once (and _does not_ guarantee at-most-once). > [!IMPORTANT] -> In the [Orleans technical report](https://research.microsoft.com/pubs/210931/Orleans-MSR-TR-2014-41.pdf) we accidentally only mentioned the 2nd option with automatic retries. We forgot to mention that by default with no retries, Orleans provides at-most-once delivery. +> The [Orleans technical report](https://research.microsoft.com/pubs/210931/Orleans-MSR-TR-2014-41.pdf) accidentally only mentioned the second option with automatic retries. It failed to mention that by default, with no retries, Orleans provides at-most-once delivery. diff --git a/docs/orleans/implementation/orleans-lifecycle.md b/docs/orleans/implementation/orleans-lifecycle.md index d46d6f5ad1280..f981027467d33 100644 --- a/docs/orleans/implementation/orleans-lifecycle.md +++ b/docs/orleans/implementation/orleans-lifecycle.md @@ -1,18 +1,19 @@ --- title: Orleans lifecycle description: Learn the various lifecycles of .NET Orleans apps. -ms.date: 07/03/2024 +ms.date: 03/30/2025 +ms.topic: conceptual --- # Orleans lifecycle overview -Some Orleans behaviors are sufficiently complex that they need ordered startup and shutdown. Some components with such behaviors include grains, silos, and clients. To address this, Orleans introduced a general component lifecycle pattern. This pattern consists of an observable lifecycle, which is responsible for signaling on stages of a component's startup and shutdown, and lifecycle observers, which are responsible for performing startup or shutdown operations at specific stages. +Some Orleans behaviors are sufficiently complex that they require ordered startup and shutdown. Components with such behaviors include grains, silos, and clients. To address this, Orleans introduced a general component lifecycle pattern. This pattern consists of an observable lifecycle, responsible for signaling stages of a component's startup and shutdown, and lifecycle observers, responsible for performing startup or shutdown operations at specific stages. For more information, see [Grain lifecycle](../grains/grain-lifecycle.md) and [Silo lifecycle](../host/silo-lifecycle.md). ## Observable lifecycle -Components that need ordered startup and shutdown can use an observable lifecycle which allows other components to observe the lifecycle and receive a notification when a stage is reached during startup or shutdown. +Components needing ordered startup and shutdown can use an observable lifecycle. This allows other components to observe the lifecycle and receive notifications when a specific stage is reached during startup or shutdown. ```csharp public interface ILifecycleObservable @@ -24,11 +25,11 @@ public interface ILifecycleObservable } ``` -The subscribe call registers an observer for notification when a stage is reached while starting or stopping. The observer's name is for reporting purposes. The stage indicated at which point in the startup/shutdown sequence the observer will be notified. Each stage of the lifecycle is observable. All observers will be notified when the stage is reached when starting and stopping. Stages are started in ascending order and stopped in descending order. The observer can unsubscribe by disposing of the returned disposable. +The subscribe call registers an observer for notification when a stage is reached during startup or shutdown. The observer's name is used for reporting purposes. The stage indicates at which point in the startup/shutdown sequence the observer receives notification. Each lifecycle stage is observable. All observers are notified when the stage is reached during startup and shutdown. Stages start in ascending order and stop in descending order. The observer can unsubscribe by disposing of the returned disposable object. ## Lifecycle observer -Components that need to take part in another component's lifecycle need to provide hooks for their startup and shutdown behaviors and subscribe to a specific stage of an observable lifecycle. +Components needing to participate in another component's lifecycle must provide hooks for their startup and shutdown behaviors and subscribe to a specific stage of an observable lifecycle. ```csharp public interface ILifecycleObserver @@ -38,15 +39,15 @@ public interface ILifecycleObserver } ``` -Both and are called when the stage subscribed to is reached during startup/shutdown. +Both and are called when the subscribed stage is reached during startup or shutdown. ## Utilities -For convenience, helper functions have been created for common lifecycle usage patterns. +For convenience, helper functions exist for common lifecycle usage patterns. ### Extensions -Extension functions exist for subscribing to observable lifecycle which doesn't require that the subscribing component implement ILifecycleObserver. Instead, these allow components to pass in lambdas or members function to be called at the subscribed stages. +Extension functions are available for subscribing to an observable lifecycle that don't require the subscribing component to implement `ILifecycleObserver`. Instead, these allow components to pass in lambdas or member functions to be called at the subscribed stages. ```csharp IDisposable Subscribe( @@ -63,7 +64,7 @@ IDisposable Subscribe( Func onStart); ``` -Similar extension functions allow generic type arguments to be used in place of the observer name. +Similar extension functions allow using generic type arguments instead of the observer name. ```csharp IDisposable Subscribe( @@ -80,7 +81,7 @@ IDisposable Subscribe( ### Lifecycle participation -Some extensibility points need a way of recognizing what components are interested in participating in a lifecycle. A lifecycle participant marker interface has been introduced for this purpose. More about how this is used will be covered when exploring silo and grain lifecycles. +Some extensibility points need a way to recognize which components are interested in participating in a lifecycle. A lifecycle participant marker interface serves this purpose. More details on its usage are covered when exploring silo and grain lifecycles. ```csharp public interface ILifecycleParticipant @@ -92,7 +93,7 @@ public interface ILifecycleParticipant ## Example -From our lifecycle tests, below is an example of a component that takes part in an observable lifecycle at multiple stages of the lifecycle. +From the Orleans lifecycle tests, below is an example of a component that participates in an observable lifecycle at multiple stages. ```csharp enum TestStages diff --git a/docs/orleans/implementation/scheduler.md b/docs/orleans/implementation/scheduler.md index 611f59041b5ce..b7b257e1e15d4 100644 --- a/docs/orleans/implementation/scheduler.md +++ b/docs/orleans/implementation/scheduler.md @@ -1,22 +1,22 @@ --- title: Scheduling overview description: Explore the scheduling overview in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 03/30/2025 +ms.topic: conceptual --- # Scheduling overview -There are two forms of scheduling in Orleans which are relevant to grains: +Two forms of scheduling in Orleans are relevant to grains: -1. Request scheduling, the scheduling of incoming grain calls for execution according to scheduling rules discussed [in Request scheduling](../grains/request-scheduling.md). -1. Task scheduling, the scheduling of synchronous blocks of code to be executed in a *single-threaded* manner +1. **Request scheduling**: Scheduling incoming grain calls for execution according to rules discussed in [Request scheduling](../grains/request-scheduling.md). +1. **Task scheduling**: Scheduling synchronous blocks of code to execute in a *single-threaded* manner. -All grain code is executed on the grain's task scheduler, which means that requests are also executed on the grain's task scheduler. -Even if the request scheduling rules allow multiple requests to execute *concurrently*, they will not execute *in parallel* because the grain's task scheduler always executes tasks one-by-one and hence never executes multiple tasks in parallel. +All grain code executes on the grain's task scheduler, meaning requests also execute on the grain's task scheduler. Even if request scheduling rules allow multiple requests to execute *concurrently*, they won't execute *in parallel* because the grain's task scheduler always executes tasks one by one and never executes multiple tasks in parallel. ## Task scheduling -To better understand scheduling, consider the following grain, `MyGrain`, which has a method called `DelayExecution()` which logs a message, waits some time, then logs another message before returning. +To better understand scheduling, consider the following grain, `MyGrain`. It has a method called `DelayExecution()` that logs a message, waits some time, then logs another message before returning. ```csharp public interface IMyGrain : IGrain @@ -41,29 +41,29 @@ public class MyGrain : Grain, IMyGrain } ``` -When this method is executed, the method body will be executed in two parts: +When this method executes, the method body executes in two parts: 1. The first `_logger.LogInformation(...)` call and the call to `Task.Delay(1_000)`. 1. The second `_logger.LogInformation(...)` call. -The second task will not be scheduled on the grain's task scheduler until the `Task.Delay(1_000)` call completes, at which point it will schedule the *continuation* of the grain method. +The second task isn't scheduled on the grain's task scheduler until the `Task.Delay(1_000)` call completes. At that point, it schedules the *continuation* of the grain method. -Here is a graphical representation of how a request is scheduled and executed as two tasks: +Here's a graphical representation of how a request is scheduled and executed as two tasks: :::image type="content" source="media/scheduler/scheduling-1.png" alt-text="Two-Task-based request execution example."::: -The above description is not specific to Orleans; it describes how task scheduling in .NET works: the compiler converts asynchronous methods in C# into an asynchronous state machine, and execution progresses through the asynchronous state machine in discrete steps. Each step is scheduled on the current (accessed via , defaulting to ) or the current . If a `TaskScheduler` is being used, each step in the method represents a `Task` instance, which is passed to that `TaskScheduler`. Therefore, a `Task` in .NET can represent two conceptual things: +The description above isn't specific to Orleans; it describes how task scheduling works in .NET. The C# compiler converts asynchronous methods into an asynchronous state machine, and execution progresses through this state machine in discrete steps. Each step schedules on the current (accessed via , defaulting to ) or the current . If a `TaskScheduler` is used, each step in the method represents a `Task` instance passed to that `TaskScheduler`. Therefore, a `Task` in .NET can represent two conceptual things: -1. An asynchronous operation that can be waited on. The execution of the `DelayExecution()` method above is represented by a `Task` which can be awaited. -1. In a synchronous block of work, each stage within the `DelayExecution()` method above is represented by a `Task`. +1. An asynchronous operation that can be awaited. The execution of the `DelayExecution()` method above is represented by a `Task` that can be awaited. +1. A synchronous block of work. Each stage within the `DelayExecution()` method above is represented by a `Task`. -When `TaskScheduler.Default` is in use, continuations are scheduled directly onto the .NET and are not wrapped in a `Task` object. The wrapping of continuations in `Task` instances occurs transparently and therefore developers rarely need to be aware of these implementation details. +When `TaskScheduler.Default` is used, continuations schedule directly onto the .NET and aren't wrapped in a `Task` object. The wrapping of continuations in `Task` instances occurs transparently, so developers rarely need to be aware of these implementation details. ### Task scheduling in Orleans -Each grain activation has its own `TaskScheduler` instance which is responsible for enforcing the *single-threaded* execution model of grains. Internally, this `TaskScheduler` is implemented via `ActivationTaskScheduler` and `WorkItemGroup`. `WorkItemGroup` keeps enqueued tasks in a where `T` is a `Task` internally and implements . To execute each currently enqueued `Task`, `WorkItemGroup` schedules *itself* on the .NET `ThreadPool`. When the .NET `ThreadPool` invokes the `WorkItemGroup`'s `IThreadPoolWorkItem.Execute()` method, the `WorkItemGroup` executes the enqueued `Task` instances one-by-one. +Each grain activation has its own `TaskScheduler` instance responsible for enforcing the *single-threaded* execution model of grains. Internally, this `TaskScheduler` is implemented via `ActivationTaskScheduler` and `WorkItemGroup`. `WorkItemGroup` keeps enqueued tasks in a (where `T` is internally a `Task`) and implements . To execute each currently enqueued `Task`, `WorkItemGroup` schedules *itself* on the .NET `ThreadPool`. When the .NET `ThreadPool` invokes the `WorkItemGroup`'s `IThreadPoolWorkItem.Execute()` method, the `WorkItemGroup` executes the enqueued `Task` instances one by one. -Each grain has a scheduler which executes by scheduling itself on the .NET `ThreadPool`: +Each grain has a scheduler that executes by scheduling itself on the .NET `ThreadPool`: :::image type="content" source="media/scheduler/scheduling-2.png" alt-text="Orleans grains scheduling themselves on the .NET ThreadPool."::: @@ -71,9 +71,9 @@ Each scheduler contains a queue of tasks: :::image type="content" source="media/scheduler/scheduling-3.png" alt-text="Scheduler queue of scheduled tasks."::: -The .NET `ThreadPool` executes each work item enqueued to it. This includes *grain schedulers* as well as other work items, such as work items scheduled via `Task.Run(...)`: +The .NET `ThreadPool` executes each work item enqueued to it. This includes *grain schedulers* as well as other work items, such as those scheduled via `Task.Run(...)`: :::image type="content" source="media/scheduler/scheduling-4.png" alt-text="Visualization of the all schedulers running in the .NET ThreadPool."::: > [!NOTE] -> A grain's scheduler can only execute on one thread at a time, but it does not always execute on the same thread. The .NET `ThreadPool` is free to use a different thread each time the grain's scheduler is executed. The grain's scheduler is responsible for making sure that it only executes on one thread at a time and this is how the *single-threaded* execution model of grains is implemented. +> A grain's scheduler can only execute on one thread at a time, but it doesn't always execute on the same thread. The .NET `ThreadPool` is free to use a different thread each time the grain's scheduler executes. The grain's scheduler ensures it only executes on one thread at a time, implementing the *single-threaded* execution model of grains. diff --git a/docs/orleans/implementation/streams-implementation/azure-queue-streams.md b/docs/orleans/implementation/streams-implementation/azure-queue-streams.md index 08c05680a555f..0d89846c9195f 100644 --- a/docs/orleans/implementation/streams-implementation/azure-queue-streams.md +++ b/docs/orleans/implementation/streams-implementation/azure-queue-streams.md @@ -1,14 +1,15 @@ --- title: Azure Queue streams overview description: Explore the streaming implementation with Azure Queue in .NET Orleans. -ms.date: 09/10/2024 +ms.date: 05/23/2025 +ms.topic: conceptual --- # Azure Queue streams overview -Each stream provider (Azure Queues, EventHub, SMS, SQS, and so on) has its queue-specific details and configuration. This section provides some details about the usage, configuration, and implementation of **Orleans Azure Queue Streams**. This section is not comprehensive, and more details are available in the streaming tests, which contain most of the configuration options, specifically [`AQClientStreamTests`](https://github.com/dotnet/orleans/tree/main/test/Extensions/TesterAzureUtils/Streaming/AQClientStreamTests.cs), [`AQSubscriptionMultiplicityTests`](https://github.com/dotnet/orleans/tree/main/test/Extensions/TesterAzureUtils/Streaming/AQSubscriptionMultiplicityTests.cs), and the extension functions for and . +Each stream provider (Azure Queues, EventHub, SMS, SQS, etc.) has its own queue-specific details and configuration. This section provides details about the usage, configuration, and implementation of **Orleans Azure Queue Streams**. This section isn't comprehensive. You can find more details in the streaming tests, which contain most configuration options, specifically [`AQClientStreamTests`](https://github.com/dotnet/orleans/tree/main/test/Extensions/TesterAzureUtils/Streaming/AQClientStreamTests.cs) and [`AQSubscriptionMultiplicityTests`](https://github.com/dotnet/orleans/tree/main/test/Extensions/TesterAzureUtils/Streaming/AQSubscriptionMultiplicityTests.cs), and the extension functions for and . -Orleans Azure Queue requires the [Microsoft.Orleans.Streaming.AzureStorage](https://www.nuget.org/packages/Microsoft.Orleans.Streaming.AzureStorage) NuGet package. In addition to the implementation, the package contains some extension methods that make the configuration at silo startup easier. The minimal configuration requires the connection string to be specified, for example: +Orleans Azure Queue requires the [Microsoft.Orleans.Streaming.AzureStorage](https://www.nuget.org/packages/Microsoft.Orleans.Streaming.AzureStorage) NuGet package. In addition to the implementation, the package contains extension methods that simplify configuration at silo startup. The minimal configuration requires specifying the connection string, for example: ```csharp hostBuilder @@ -32,9 +33,9 @@ hostBuilder }) ``` -The pulling agents pull repeatedly until there are no more messages on a queue, then delay for a configurable period before continuing to pull. This process occurs for each queue. Internally, the pulling agents place messages in a cache (one cache per queue) for delivery to consumers, but will stop reading if the cache fills up. Messages are removed from the cache once consumers process the messages, so the read rate should roughly be throttled by the processing rate of the consumers. +The pulling agents pull repeatedly until there are no more messages on a queue, then delay for a configurable period before continuing to pull. This process occurs for each queue. Internally, the pulling agents place messages in a cache (one cache per queue) for delivery to consumers but stop reading if the cache fills up. Messages are removed from the cache once consumers process them, so the read rate should be roughly throttled by the consumers' processing rate. -By default, Orleans Azure Queue uses **8 queues** (see ) and 8 related pulling agents, a delay of **100ms** (see ), and a cache size () of **4096 messages** (see ). +By default, Orleans Azure Queue uses **8 queues** (see ) and 8 related pulling agents, a delay of **100 ms** (see ), and a cache size () of **4096 messages** (see ). ## Configuration @@ -68,29 +69,29 @@ In a production system, you may need to tune the default configuration. When tun Assuming a system with these characteristics: -* 100 streams, -* 10 queues, -* Each stream processing 60 messages per minute, -* Each message takes around 30ms to process, -* 1 minute worth of messages in cache (cache time). +- 100 streams, +- 10 queues, +- Each stream processing 60 messages per minute, +- Each message takes around 30ms to process, +- 1 minute worth of messages in cache (cache time). So we can calculate some parameters of the system: -* Streams/queue: Even balancing of streams across queues would be an ideal 10 streams/queue (100 streams / 10 queues). +- Streams/queue: Even balancing of streams across queues would be an ideal 10 streams/queue (100 streams / 10 queues). But since streams won't always be evenly balanced over the queues, doubling (or even tripling) the ideal is safer than expecting ideal distribution. Hence **20 streams/queue** (10 streams/queue x 2 as safety factor) is probably reasonable. -* Messages/minute: This means each queue will be expected to process up to **1200 messages/minute** (60 messages x 20 streams). +- Messages/minute: This means each queue will be expected to process up to **1200 messages/minute** (60 messages x 20 streams). Then we can determine the visibility time to use: -* Visibility time: The cache time (1 minute) is configured to hold 1 minute of messages (so 1200 messages, as we calculated messages/minute above). +- Visibility time: The cache time (1 minute) is configured to hold 1 minute of messages (so 1200 messages, as we calculated messages/minute above). We assumed that each message takes 30 ms to process, then we can expect messages to spend up to 36 seconds in the cache (0.030 sec x 1200 msg = 36 sec), so the visibility time - doubled for safety - would need be over 72 seconds (36 sec of time in cache x 2). Accordingly, if we define a bigger cache, that would require a longer visibility time. Final considerations in a real-world system: -* Since the order is only per stream, and a queue consumes many streams, messages will likely be processed across multiple streams in parallel (as an example: we have a grain for the stream, which can run in parallel). This means we'll burn through the cache in far less time, but we planned for the worse case: it will give the system room to continue to function well even under intermittent delays and transient errors. +- Since the order is only per stream, and a queue consumes many streams, messages will likely be processed across multiple streams in parallel (as an example: we have a grain for the stream, which can run in parallel). This means we'll burn through the cache in far less time, but we planned for the worse case: it will give the system room to continue to function well even under intermittent delays and transient errors. So we can configure Azure Queue Streams using: diff --git a/docs/orleans/implementation/testing.md b/docs/orleans/implementation/testing.md index 0d008ba75afda..1d7e5d529b5ee 100644 --- a/docs/orleans/implementation/testing.md +++ b/docs/orleans/implementation/testing.md @@ -1,20 +1,21 @@ --- title: Unit testing description: Learn how to unit test with .NET Orleans. -ms.date: 07/03/2024 +ms.date: 03/30/2025 +ms.topic: how-to --- # Unit testing with Orleans -This tutorial shows how to unit test your grains to make sure they behave correctly. There are two main ways to unit test your grains, and the method you choose will depend on the type of functionality you're testing. The [Microsoft.Orleans.TestingHost](https://www.nuget.org/packages/Microsoft.Orleans.TestingHost) NuGet package can be used to create test silos for your grains, or you can use a mocking framework like [Moq](https://github.com/moq/moq) to mock parts of the Orleans runtime that your grain interacts with. +This tutorial shows how to unit test your grains to ensure they behave correctly. There are two main ways to unit test your grains, and the method you choose depends on the type of functionality you're testing. Use the [Microsoft.Orleans.TestingHost](https://www.nuget.org/packages/Microsoft.Orleans.TestingHost) NuGet package to create test silos for your grains, or use a mocking framework like [Moq](https://github.com/moq/moq) to mock parts of the Orleans runtime your grain interacts with. ## Use the `TestCluster` -The `Microsoft.Orleans.TestingHost` NuGet package contains which can be used to create an in-memory cluster, comprised of two silos by default, which can be used to test grains. +The `Microsoft.Orleans.TestingHost` NuGet package contains , which you can use to create an in-memory cluster (comprised of two silos by default) for testing grains. :::code source="snippets/testing/Orleans-testing/Sample.OrleansTesting/HelloGrainTests.cs"::: -Due to the overhead of starting an in-memory cluster, you may wish to create a `TestCluster` and reuse it among multiple test cases. For example, this can be done using xUnit's class or collection fixtures. +Due to the overhead of starting an in-memory cluster, you might want to create a `TestCluster` and reuse it among multiple test cases. For example, achieve this using xUnit's class or collection fixtures. To share a `TestCluster` between multiple test cases, first create a fixture type: @@ -28,17 +29,17 @@ You can now reuse a `TestCluster` in your test cases: :::code source="snippets/testing/Orleans-testing/Sample.OrleansTesting/HelloGrainTestsWithFixture.cs"::: -When all tests have been completed and the in-memory cluster silos are stopped, xUnit calls the method of the `ClusterFixture` type. `TestCluster` also has a constructor that accepts that you can use to configure the silos in the cluster. +When all tests complete and the in-memory cluster silos stop, xUnit calls the method of the `ClusterFixture` type. `TestCluster` also has a constructor accepting that you can use to configure the silos in the cluster. -If you're using Dependency Injection in your Silo to make services available to Grains, you can use this pattern as well: +If you use Dependency Injection in your Silo to make services available to Grains, you can use this pattern as well: :::code source="snippets/testing/Orleans-testing/Sample.OrleansTesting/ClusterFixtureWithConfig.cs" ## Use mocks -Orleans also makes it possible to mock many parts of the system, and for many scenarios, this is the easiest way to unit test grains. This approach does have limitations (for example, around scheduling reentrancy and serialization) and may require that grains include code used only by your unit tests. The [Orleans TestKit](https://github.com/OrleansContrib/OrleansTestKit) provides an alternative approach, which side-steps many of these limitations. +Orleans also allows mocking many parts of the system. For many scenarios, this is the easiest way to unit test grains. This approach has limitations (e.g., around scheduling reentrancy and serialization) and might require grains to include code used only by your unit tests. The [Orleans TestKit](https://github.com/OrleansContrib/OrleansTestKit) provides an alternative approach that sidesteps many of these limitations. -For example, imagine that the grain you're testing interacts with other grains. To be able to mock those other grains, you also need to mock the member of the grain under test. By default `GrainFactory` is a normal `protected` property, but most mocking frameworks require properties to be `public` and `virtual` to be able to mock them. So the first thing you need to do is make `GrainFactory` both a `public` and `virtual` property: +For example, imagine the grain you're testing interacts with other grains. To mock those other grains, you also need to mock the member of the grain under test. By default, `GrainFactory` is a normal `protected` property, but most mocking frameworks require properties to be `public` and `virtual` to enable mocking. So, the first step is to make `GrainFactory` both `public` and `virtual`: ```csharp public new virtual IGrainFactory GrainFactory @@ -47,7 +48,7 @@ public new virtual IGrainFactory GrainFactory } ``` -Now you can create your grain outside of the Orleans runtime and use mocking to control the behavior of `GrainFactory`: +Now you can create your grain outside the Orleans runtime and use mocking to control the behavior of `GrainFactory`: ```csharp using Xunit; @@ -74,4 +75,4 @@ public class WorkerGrainTests } ``` -Here you create the grain under test, `WorkerGrain`, using Moq, which means you can override the behavior of the `GrainFactory` so that it returns a mocked `IJournalGrain`. You can then verify that the `WorkerGrain` interacts with the `IJournalGrain` as you expect. +Here, create the grain under test, `WorkerGrain`, using Moq. This allows overriding the `GrainFactory`'s behavior so it returns a mocked `IJournalGrain`. You can then verify that `WorkerGrain` interacts with `IJournalGrain` as expected. diff --git a/docs/orleans/migration-guide.md b/docs/orleans/migration-guide.md index f1bcd6f565a7c..5e4541938d320 100644 --- a/docs/orleans/migration-guide.md +++ b/docs/orleans/migration-guide.md @@ -1,7 +1,9 @@ --- title: Migrate from Orleans 3.x to 7.0 -description: Learn the various new features introduced in Orleans 7.0, and how to migrate from 3.x versions. -ms.date: 07/03/2024 +description: Learn about the new features in Orleans 7.0 and how to migrate your application from version 3.x. +ms.date: 03/30/2025 +ms.topic: how-to +ms.custom: migration-guide --- # Migrate from Orleans 3.x to 7.0 @@ -10,15 +12,15 @@ Orleans 7.0 introduces several beneficial changes, including improvements to hos ## Migration -Due to changes in how Orleans identifies grains and streams, you cannot (currently) easily migrate existing applications using reminders, streams, or grain persistence to Orleans 7.0. +Due to changes in how Orleans identifies grains and streams, migrating existing applications using reminders, streams, or grain persistence to Orleans 7.0 isn't currently straightforward. -Applications running previous versions of Orleans cannot be smoothly upgraded via a rolling upgrade to Orleans 7.0. Therefore, a different upgrade strategy must be used, such as deploying a new cluster and decommissioning the previous cluster. Orleans 7.0 changes the wire protocol in an incompatible fashion, meaning that clusters cannot contain a mix of Orleans 7.0 hosts and hosts running previous versions of Orleans. +Smoothly upgrading applications running previous Orleans versions via a rolling upgrade to Orleans 7.0 isn't possible. Therefore, use a different upgrade strategy, such as deploying a new cluster and decommissioning the previous one. Orleans 7.0 changes the wire protocol incompatibly, meaning clusters cannot contain a mix of Orleans 7.0 hosts and hosts running previous Orleans versions. -We have avoided such breaking changes for many years, even across major releases, so why now? There are two major reasons: identities and serialization. Regarding identities, Grain and stream identities are now comprised of strings, allowing grains to encode generic type information properly and allowing streams to map more easily to the application domain. Grain types were previously identified using a complex data structure that could not represent generic grains, leading to corner cases. Streams were identified by a `string` namespace and a key, which was difficult for developers to map to their application domain, however efficient. Serialization is now version-tolerant, meaning that you can modify your types in certain compatible ways, following a set of rules, and be confident that you can upgrade your application without serialization errors. This was especially problematic when application types persisted in streams or grain storage. The following sections detail the major changes and discuss them in more detail. +Such breaking changes have been avoided for many years, even across major releases. Why now? There are two major reasons: identities and serialization. Regarding identities, grain and stream identities now consist of strings. This allows grains to encode generic type information properly and makes mapping streams to the application domain easier. Previously, Orleans identified grain types using a complex data structure that couldn't represent generic grains, leading to corner cases. Streams were identified by a `string` namespace and a key, which was efficient but difficult to map to the application domain. Serialization is now version-tolerant. This means types can be modified in certain compatible ways, following a set of rules, with confidence that the application can be upgraded without serialization errors. This capability is especially helpful when application types persist in streams or grain storage. The following sections detail the major changes and discuss them further. ### Packaging changes -If you're upgrading a project to Orleans 7.0, you'll need to perform the following actions: +When upgrading a project to Orleans 7.0, perform the following actions: - All clients should reference [Microsoft.Orleans.Client](https://nuget.org/packages/Microsoft.Orleans.Client). - All silos (servers) should reference [Microsoft.Orleans.Server](https://nuget.org/packages/Microsoft.Orleans.Server). @@ -30,17 +32,17 @@ If you're upgrading a project to Orleans 7.0, you'll need to perform the followi - Remove all references to `Microsoft.Orleans.OrleansRuntime`. - The [Microsoft.Orleans.Server](https://nuget.org/packages/Microsoft.Orleans.Server) packages reference its replacement, `Microsoft.Orleans.Runtime`. - Remove calls to `ConfigureApplicationParts`. -_Application Parts_ has been removed. The C# Source Generator for Orleans is added to all packages (including the client and server) and will generate the equivalent of _Application Parts_ automatically. + _Application Parts_ have been removed. The C# Source Generator for Orleans is added to all packages (including the client and server) and automatically generates the equivalent of _Application Parts_. - Replace references to `Microsoft.Orleans.OrleansServiceBus` with [Microsoft.Orleans.Streaming.EventHubs](https://nuget.org/packages/Microsoft.Orleans.Streaming.EventHubs). -- If you are using reminders, add a reference to [Microsoft.Orleans.Reminders](https://nuget.org/packages/Microsoft.Orleans.Reminders). -- If you are using streams, add a reference to [Microsoft.Orleans.Streaming](https://nuget.org/packages/Microsoft.Orleans.Streaming). +- If using reminders, add a reference to [Microsoft.Orleans.Reminders](https://nuget.org/packages/Microsoft.Orleans.Reminders). +- If using streams, add a reference to [Microsoft.Orleans.Streaming](https://nuget.org/packages/Microsoft.Orleans.Streaming). > [!TIP] > All of the Orleans samples have been upgraded to Orleans 7.0 and can be used as a reference for what changes were made. For more information, see [Orleans issue #8035](https://github.com/dotnet/orleans/issues/8035) that itemizes the changes made to each sample. -## Orleans `global using` directives +## Orleans global using directives -All Orleans projects either directly or indirectly reference the `Microsoft.Orleans.Sdk` NuGet package. When an Orleans project has configured to _enable_ implicit usings (for example `enable`), the `Orleans` and `Orleans.Hosting` namespaces are both implicitly used. This means that your app code doesn't need these directives. +All Orleans projects either directly or indirectly reference the `Microsoft.Orleans.Sdk` NuGet package. When an Orleans project is configured to _enable_ implicit usings (for example, `enable`), the project implicitly uses both the `Orleans` and `Orleans.Hosting` namespaces. This means app code doesn't need these `using` directives. For more information, see [ImplicitUsings](../core/project-sdk/msbuild-props.md#implicitusings) and [dotnet/orleans/src/Orleans.Sdk/build/Microsoft.Orleans.Sdk.targets](https://github.com/dotnet/orleans/blob/main/src/Orleans.Sdk/build/Microsoft.Orleans.Sdk.targets#L4-L5). @@ -48,16 +50,16 @@ For more information, see [ImplicitUsings](../core/project-sdk/msbuild-props.md# ## Hosting -The type has been replaced with a extension method on . The `IHostBuilder` type comes from the [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting) NuGet package. This means that you can add an Orleans client to an existing host without having to create a separate dependency injection container. The client connects to the cluster during startup. Once has completed, the client will be connected automatically. Services added to the `IHostBuilder` are started in the order of registration, so call `UseOrleansClient` before calling will ensure Orleans is started before ASP.NET Core starts for example, allowing you to access the client from your ASP.NET Core application immediately. +The type is replaced with the extension method on . The `IHostBuilder` type comes from the [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting) NuGet package. This means an Orleans client can be added to an existing host without creating a separate dependency injection container. The client connects to the cluster during startup. Once completes, the client connects automatically. Services added to the `IHostBuilder` start in the order of registration. Calling `UseOrleansClient` before calling , for example, ensures Orleans starts before ASP.NET Core starts, allowing immediate access to the client from the ASP.NET Core application. -If you wish to emulate the previous `ClientBuilder` behavior, you can create a separate `HostBuilder` and configure it with an Orleans client. `IHostBuilder` can have either an Orleans client or an Orleans silo configured. All silos register an instance of and which the application can use, so configuring a client separately is unnecessary and unsupported. +To emulate the previous `ClientBuilder` behavior, create a separate `HostBuilder` and configure it with an Orleans client. An `IHostBuilder` can be configured with either an Orleans client or an Orleans silo. All silos register an instance of and that the application can use, so configuring a client separately is unnecessary and unsupported. ## `OnActivateAsync` and `OnDeactivateAsync` signature change -Orleans allows grains to execute code during activation and deactivation. This can be used to perform tasks such as reading state from storage or log lifecycle messages. In Orleans 7.0, the signature of these lifecycle methods changed: +Orleans allows grains to execute code during activation and deactivation. Use this capability to perform tasks such as reading state from storage or logging lifecycle messages. In Orleans 7.0, the signature of these lifecycle methods changed: -- now accepts a parameter. When the is canceled, the activation process should be abandoned. -- now accepts a parameter and a `CancellationToken` parameter. The `DeactivationReason` indicates why the activation is being deactivated. Developers are expected to use this information for logging and diagnostics purposes. When the `CancellationToken` is canceled, the deactivation process should be completed promptly. Note that since any host can fail at any time, it is not recommended to rely on `OnDeactivateAsync` to perform important actions such as persisting critical state. +- now accepts a parameter. When the is canceled, abandon the activation process. +- now accepts a parameter and a `CancellationToken` parameter. The `DeactivationReason` indicates why the activation is being deactivated. Use this information for logging and diagnostics purposes. When the `CancellationToken` is canceled, complete the deactivation process promptly. Note that since any host can fail at any time, relying on `OnDeactivateAsync` to perform important actions, such as persisting critical state, isn't recommended. Consider the following example of a grain overriding these new methods: @@ -85,7 +87,7 @@ public sealed class PingGrain : Grain, IPingGrain } ``` -## POCO Grains and `IGrainBase` +## POCO grains and `IGrainBase` Grains in Orleans no longer need to inherit from the base class or any other class. This functionality is referred to as [POCO](../standard/glossary.md#poco) grains. To access extension methods such as any of the following: @@ -99,7 +101,7 @@ Grains in Orleans no longer need to inherit from the base c - - -Your grain must either implement or inherit from . Here is an example of implementing `IGrainBase` on a grain class: +The grain must either implement or inherit from . Here's an example of implementing `IGrainBase` on a grain class: ```csharp public sealed class PingGrain : IGrainBase, IPingGrain @@ -112,7 +114,7 @@ public sealed class PingGrain : IGrainBase, IPingGrain } ``` -`IGrainBase` also defines `OnActivateAsync` and `OnDeactivateAsync` with default implementations, allowing your grain to participate in its lifecycle if desired: +`IGrainBase` also defines `OnActivateAsync` and `OnDeactivateAsync` with default implementations, allowing the grain to participate in its lifecycle if desired: ```csharp public sealed class PingGrain : IGrainBase, IPingGrain @@ -145,7 +147,7 @@ public sealed class PingGrain : IGrainBase, IPingGrain ## Serialization -The most burdensome change in Orleans 7.0 is the introduction of the version-tolerant serializer. This change was made because applications tend to evolve and this led to a significant pitfall for developers, since the previous serializer couldn't tolerate adding properties to existing types. On the other hand, the serializer was flexible, allowing developers to represent most .NET types without modification, including features such as generics, polymorphism, and reference tracking. A replacement was long overdue, but users still need the high-fidelity representation of their types. Therefore, a replacement serializer was introduced in Orleans 7.0 which supports the high-fidelity representation of .NET types while also allowing types to evolve. The new serializer is much more efficient than the previous serializer, resulting in up to 170% higher end-to-end throughput. +The most burdensome change in Orleans 7.0 is the introduction of the version-tolerant serializer. This change was made because applications tend to evolve, which led to a significant pitfall for developers since the previous serializer couldn't tolerate adding properties to existing types. On the other hand, the previous serializer was flexible, allowing representation of most .NET types without modification, including features such as generics, polymorphism, and reference tracking. A replacement was long overdue, but high-fidelity representation of types is still needed. Therefore, Orleans 7.0 introduces a replacement serializer supporting high-fidelity representation of .NET types while also allowing types to evolve. The new serializer is much more efficient than the previous one, resulting in up to 170% higher end-to-end throughput. For more information, see the following articles as it relates to Orleans 7.0: @@ -155,7 +157,7 @@ For more information, see the following articles as it relates to Orleans 7.0: ## Grain identities -Grains each have a unique identity which is comprised of the grain's type and its key. Previous versions of Orleans used a compound type for `GrainId`s to support grain keys of either: +Grains each have a unique identity comprised of the grain's type and its key. Previous Orleans versions used a compound type for `GrainId`s to support grain keys of either: - - [`long`](xref:System.Int64) @@ -163,33 +165,33 @@ Grains each have a unique identity which is comprised of the grain's type and it - + [string](xref:System.String) - [`long`](xref:System.Int64) + [string](xref:System.String) -This involves some complexity when it comes to dealing with grain keys. Grain identities consist of two components: a type and a key. The type component previously consisted of a numeric type code, a category, and 3 bytes of generic type information. +This approach involves some complexity when dealing with grain keys. Grain identities consist of two components: a type and a key. The type component previously consisted of a numeric type code, a category, and 3 bytes of generic type information. -Grain identities now take the form `type/key` where both `type` and `key` are strings. The most commonly used grain key interface is the . This greatly simplifies how grain identity works and improves support for generic grain types. +Grain identities now take the form `type/key`, where both `type` and `key` are strings. The most commonly used grain key interface is . This greatly simplifies how grain identity works and improves support for generic grain types. -Grain interfaces are also now represented using a human-readable name, rather than a combination of a hash code and a string representation of any generic type parameters. +Grain interfaces are now also represented using a human-readable name, rather than a combination of a hash code and a string representation of any generic type parameters. -The new system is more customizable and these customizations can be driven by attributes. +The new system is more customizable, and these customizations can be driven with attributes. -- on a grain `class` specifies the *Type* portion of its grain id. -- on a grain `interface` specifies the *Type* of the grain which should resolve by default when getting a grain reference. For example, when calling `IGrainFactory.GetGrain("my-key")`, the grain factory will return a reference to the grain `"my-type/my-key"` if `IMyGrain` has the aforementioned attribute specified. -- allows overriding the interface name. Specifying a name explicitly using this mechanism allows renaming of the interface type without breaking compatibility with existing grain references. Note that your interface should also have the in this case, since its identity may be serialized. For more information on specifying a type alias, see the section on serialization. +- on a grain `class` specifies the *Type* portion of its grain ID. +- on a grain `interface` specifies the *Type* of the grain that should resolve by default when getting a grain reference. For example, when calling `IGrainFactory.GetGrain("my-key")`, the grain factory returns a reference to the grain `"my-type/my-key"` if `IMyGrain` has the aforementioned attribute specified. +- allows overriding the interface name. Specifying a name explicitly using this mechanism allows renaming the interface type without breaking compatibility with existing grain references. Note that the interface should also have the in this case, since its identity might be serialized. For more information on specifying a type alias, see the section on serialization. -As mentioned above, overriding the default grain class and interface names for your types allows you to rename the underlying types without breaking compatibility with existing deployments. +As mentioned above, overriding the default grain class and interface names for types allows renaming the underlying types without breaking compatibility with existing deployments. ## Stream identities -When Orleans streams were first released, streams could only be identified using a . This was efficient in terms of memory allocation, but it was difficult for users to create meaningful stream identities, often requiring some encoding or indirection to determine the appropriate stream identity for a given purpose. +When Orleans streams were first released, streams could only be identified using a . This approach was efficient in terms of memory allocation but made creating meaningful stream identities difficult, often requiring some encoding or indirection to determine the appropriate stream identity for a given purpose. -In Orleans 7.0, streams are now identified using strings. The `struct` contains three properties: a , a , and a . These property values are encoded UTF-8 strings. For example, . +In Orleans 7.0, streams are identified using strings. The `struct` contains three properties: , , and . These property values are encoded UTF-8 strings. For example, see . ### Replacement of SimpleMessageStreams with BroadcastChannel -`SimpleMessageStreams` (also called SMS) was removed in 7.0. SMS had the same interface as , but its behavior was very different, since it relied on direct grain-to-grain calls. To avoid confusion, SMS was removed, and a new replacement called was introduced. +`SimpleMessageStreams` (also called SMS) is removed in 7.0. SMS had the same interface as , but its behavior was very different because it relied on direct grain-to-grain calls. To avoid confusion, SMS was removed and a new replacement called was introduced. -`BroadcastChannel` only supports implicit subscriptions and can be a direct replacement in this case. If you need explicit subscriptions or need to use the `PersistentStream` interface (for example you were using SMS in tests while using `EventHub` in production), then `MemoryStream` is the best candidate for you. +`BroadcastChannel` only supports implicit subscriptions and can be a direct replacement in this case. If explicit subscriptions are needed or the `PersistentStream` interface must be used (for example, if SMS was used in tests while `EventHub` was used in production), then `MemoryStream` is the best candidate. -`BroadcastChannel` will have the same behaviors as SMS, while `MemoryStream` will behave like other stream providers. Consider the following Broadcast Channel usage example: +`BroadcastChannel` has the same behaviors as SMS, while `MemoryStream` behaves like other stream providers. Consider the following Broadcast Channel usage example: ```csharp // Configuration @@ -236,7 +238,7 @@ public sealed class SimpleSubscriberGrain : Grain, ISubscriberGrain, IOnBroadcas } ``` -Migration to `MemoryStream` will be easier, since only the configuration needs to change. Consider the following `MemoryStream` configuration: +Migration to `MemoryStream` is easier since only the configuration needs changing. Consider the following `MemoryStream` configuration: ```csharp builder.AddMemoryStreams( @@ -251,17 +253,17 @@ builder.AddMemoryStreams( ## OpenTelemetry -The telemetry system has been updated in Orleans 7.0 and the previous system has been removed in favor of standardized .NET APIs such as .NET Metrics for metrics and for tracing. +The telemetry system is updated in Orleans 7.0, and the previous system is removed in favor of standardized .NET APIs such as .NET Metrics for metrics and for tracing. -As a part of this, the existing `Microsoft.Orleans.TelemetryConsumers.*` packages have been removed. We are considering introducing a new set of packages to streamline the process of integrating the metrics emitted by Orleans into your monitoring solution of choice. As always, feedback and contributions are welcome. +As part of this, the existing `Microsoft.Orleans.TelemetryConsumers.*` packages are removed. A new set of packages is being considered to streamline integrating metrics emitted by Orleans into the monitoring solution of choice. As always, feedback and contributions are welcome. -The `dotnet-counters` tool features performance monitoring for ad-hoc health monitoring and first-level performance investigation. For Orleans counters, the [dotnet-counters](../core/diagnostics/dotnet-counters.md) tool can be used to monitor them: +The `dotnet-counters` tool features performance monitoring for ad-hoc health monitoring and first-level performance investigation. For Orleans counters, use the [dotnet-counters](../core/diagnostics/dotnet-counters.md) tool to monitor them: ```dotnetcli dotnet counters monitor -n MyApp --counters Microsoft.Orleans ``` -Similarly, OpenTelemetry metrics can add the `Microsoft.Orleans` meters, as shown in the following code: +Similarly, add the `Microsoft.Orleans` meters to OpenTelemetry metrics, as shown in the following code: ```csharp builder.Services.AddOpenTelemetry() @@ -270,7 +272,7 @@ builder.Services.AddOpenTelemetry() .AddMeter("Microsoft.Orleans")); ``` -To enable distributed tracing, you configure OpenTelemetry as shown in the following code: +To enable distributed tracing, configure OpenTelemetry as shown in the following code: ```csharp builder.Services.AddOpenTelemetry() @@ -306,13 +308,13 @@ builder.Host.UseOrleans((_, clientBuilder) => ## Refactor features from core package into separate packages -In Orleans 7.0, we have made an effort to factor extensions into separate packages which don't rely on . Namely, , , and have been separated from the core. This means that these packages are entirely *pay* for what you _use_ and no code in the core of Orleans is dedicated to these features. This slims down the core API surface and assembly size, simplifies the core, and improves performance. Regarding performance, Transactions in Orleans previously required some code which was executed for every method to coordinate potential transactions. That has since been moved to per-method. +In Orleans 7.0, extensions were factored into separate packages that don't rely on . Namely, , , and were separated from the core. This means these packages are entirely *pay* for what is _used_, and no code in the Orleans core is dedicated to these features. This approach slims down the core API surface and assembly size, simplifies the core, and improves performance. Regarding performance, transactions in Orleans previously required some code executing for every method to coordinate potential transactions. That coordination logic is now moved to a per-method basis. -This is a compilation-breaking change. You may have existing code that interacts with reminders or streams by calling into methods which were previously defined on the base class but are now extension methods. Such calls which do not specify `this` (for example ) will need to be updated to include `this` (for example `this.GetReminders()`) because extension methods must be qualified. There will be a compilation error if you do not update those calls and the required code change may not be obvious if you do not know what has changed. +This is a compilation-breaking change. Existing code interacting with reminders or streams by calling methods previously defined on the base class might break because these are now extension methods. Update such calls that don't specify `this` (e.g., ) to include `this` (e.g., `this.GetReminders()`) because extension methods must be qualified. A compilation error occurs if these calls aren't updated, and the required code change might not be obvious without knowing what changed. ## Transaction client -Orleans 7.0 introduces a new abstraction for coordinating transactions, . Previously, transactions could only be coordinated by grains. With `ITransactionClient`, which is available via dependency injection, clients can also coordinate transactions without needing an intermediary grain. The following example withdraws credits from one account and deposits them into another within a single transaction. This code can be called from within a grain or from an external client which has retrieved the `ITransactionClient` from the dependency injection container. +Orleans 7.0 introduces a new abstraction for coordinating transactions: . Previously, only grains could coordinate transactions. With `ITransactionClient`, available via dependency injection, clients can also coordinate transactions without needing an intermediary grain. The following example withdraws credits from one account and deposits them into another within a single transaction. Call this code from within a grain or from an external client that retrieved the `ITransactionClient` from the dependency injection container. ```csharp await transactionClient.RunTransaction( @@ -320,7 +322,7 @@ await transactionClient.RunTransaction( () => Task.WhenAll(from.Withdraw(100), to.Deposit(100))); ``` -For transactions coordinated by the client, the client must add the required services during configuration time: +For transactions coordinated by the client, the client must add the required services during configuration: ``` csharp clientBuilder.UseTransactions(); @@ -330,11 +332,11 @@ The [BankAccount](https://github.com/dotnet/samples/tree/main/orleans/BankAccoun ## Call chain reentrancy -Grains are single-threaded and process requests one by one from beginning to completion by default. In other words, grains are not reentrant by default. Adding the to a grain class allows for multiple requests be processed concurrently, in an interleaving fashion, while still being single-threaded. This can be useful for grains that hold no internal state or perform a lot of asynchronous operations, such as issuing HTTP calls or writing to a database. Extra care needs to be taken when requests can interleave: it's possible that the state of a grain is observed before an `await` statement has changed by the time the asynchronous operation completes and the method resumes execution. +Grains are single-threaded and process requests one by one from beginning to completion by default. In other words, grains are not reentrant by default. Adding the to a grain class allows the grain to process multiple requests concurrently in an interleaving fashion while still being single-threaded. This capability can be useful for grains holding no internal state or performing many asynchronous operations, such as issuing HTTP calls or writing to a database. Extra care is needed when requests can interleave: it's possible that a grain's state observed before an `await` statement changes by the time the asynchronous operation completes and the method resumes execution. -For example, the following grain represents a counter. It has been marked `Reentrant`, allowing multiple calls to interleave. The `Increment()` method should increment the internal counter and return the observed value. However, since the `Increment()` method body observes the grain's state before an `await` point and updates it afterwards, it is possible that multiple interleaving executions of `Increment()` can result in a `_value` less than the total number of `Increment()` calls received. This is an error introduced by improper use of reentrancy. +For example, the following grain represents a counter. It's marked `Reentrant`, allowing multiple calls to interleave. The `Increment()` method should increment the internal counter and return the observed value. However, because the `Increment()` method body observes the grain's state before an `await` point and updates it afterward, multiple interleaving executions of `Increment()` can result in a `_value` less than the total number of `Increment()` calls received. This is an error introduced by improper use of reentrancy. -Removing the is enough to fix the problem. +Removing the is enough to fix this problem. ```csharp [Reentrant] @@ -356,12 +358,12 @@ public sealed class CounterGrain : Grain, ICounterGrain } ``` -To prevent such errors, grains are non-reentrant by default. The downside to this is reduced throughput for grains that perform asynchronous operations in their implementation, since other requests cannot be processed while the grain is waiting for an asynchronous operation to complete. To alleviate this, Orleans offers several options to allow reentrancy in certain cases: +To prevent such errors, grains are non-reentrant by default. The downside is reduced throughput for grains performing asynchronous operations in their implementation, since the grain cannot process other requests while waiting for an asynchronous operation to complete. To alleviate this, Orleans offers several options to allow reentrancy in certain cases: -- For an entire class: placing the on the grain allows any request to the grain to interleave with any other request. -- For a subset of methods: placing the on the grain *interface* method allows requests to that method to interleave with any other request and for requests to that method to be interleaved by any other request. -- For a subset of methods: placing the on the grain *interface* method allows requests to that method to interleave with any other `ReadOnly` request and for requests to that method to be interleaved by any other `ReadOnly` request. In this sense, it is a more restricted form of `AlwaysInterleave`. -- For any request within a call chain: and on the grain allows any request to the grain to interleave with any other request. +- For a subset of methods: Placing the on the grain *interface* method allows requests to that method to interleave with any other request and allows any other request to interleave requests to that method. +- For a subset of methods: Placing the on the grain *interface* method allows requests to that method to interleave with any other `ReadOnly` request and allows any other `ReadOnly` request to interleave requests to that method. In this sense, it's a more restricted form of `AlwaysInterleave`. +- For any request within a call chain: and allow opting in and out of allowing downstream requests to reenter the grain. Both calls return a value that _must_ be disposed of when exiting the request. Therefore, use them as follows: ``` csharp public Task OuterCall(IMyGrain other) @@ -381,16 +383,16 @@ public Task CallMeBack(IMyGrain grain) public Task InnerCall() => Task.CompletedTask; ``` -Call-chain reentrancy must be opted-in per-grain, per-call-chain. For example, consider two grains, grain A & grain B. If grain A enables call chain reentrancy before calling grain B, grain B can call back into grain A in that call. However, grain A cannot call back into grain B if grain B has not *also* enabled call chain reentrancy. It is per-grain, per-call-chain. +Opt-in to call-chain reentrancy per-grain, per-call-chain. For example, consider two grains, A and B. If grain A enables call chain reentrancy before calling grain B, grain B can call back into grain A in that call. However, grain A cannot call back into grain B if grain B hasn't *also* enabled call chain reentrancy. It's enabled per-grain, per-call-chain. -Grains can also suppress call chain reentrancy information from flowing down a call chain using `using var _ = RequestContext.SuppressCallChainReentrancy()`. This prevents subsequent calls from reentry. +Grains can also suppress call chain reentrancy information from flowing down a call chain using `using var _ = RequestContext.SuppressCallChainReentrancy()`. This prevents subsequent calls from reentering. ### ADO.NET migration scripts -To ensure forward compatibility with Orleans clustering, persistence, and reminders that rely on ADO.NET, you'll need the appropriate SQL migration script: +To ensure forward compatibility with Orleans clustering, persistence, and reminders relying on ADO.NET, the appropriate SQL migration script is needed: -* [Clustering](https://github.com/dotnet/orleans/tree/main/src/AdoNet/Orleans.Clustering.AdoNet/Migrations) -* [Persistence](https://github.com/dotnet/orleans/tree/main/src/AdoNet/Orleans.Persistence.AdoNet/Migrations) -* [Reminders](https://github.com/dotnet/orleans/tree/main/src/AdoNet/Orleans.Reminders.AdoNet/Migrations) +- [Clustering](https://github.com/dotnet/orleans/tree/main/src/AdoNet/Orleans.Clustering.AdoNet/Migrations) +- [Persistence](https://github.com/dotnet/orleans/tree/main/src/AdoNet/Orleans.Persistence.AdoNet/Migrations) +- [Reminders](https://github.com/dotnet/orleans/tree/main/src/AdoNet/Orleans.Reminders.AdoNet/Migrations) Select the files for the database used and apply them in order. diff --git a/docs/orleans/overview.md b/docs/orleans/overview.md index 81066496d1c3a..ab4d34228295f 100644 --- a/docs/orleans/overview.md +++ b/docs/orleans/overview.md @@ -1,42 +1,41 @@ --- title: Orleans overview description: An introduction to .NET Orleans. -ms.date: 07/03/2024 +ms.date: 03/30/2025 +ms.topic: overview --- # Microsoft Orleans -Orleans: +Orleans is a cross-platform framework designed to simplify building distributed apps. Whether scaling from a single server to thousands of cloud-based apps, Orleans provides tools to help manage the complexities of distributed systems. It extends familiar C# concepts to multi-server environments, allowing developers to focus on the app's logic. -* Is a cross-platform framework for building robust, scalable distributed apps. Distributed apps are defined as apps that span more than a single process, often beyond hardware boundaries using peer-to-peer communication. -* Scales from a single on-premises server to thousands of distributed, highly available apps in the cloud. -* Extends familiar concepts and C# idioms to multi-server environments. -* Is designed to scale elastically. When a host joins a cluster, it can accept new activations. When a host leaves the cluster, the previous activations on that host will be reactivated on the remaining hosts as needed. A host may leave a cluster because of scale down or a machine failure. An Orleans cluster can be scaled down to a single host. The same properties that enable elastic scalability enable fault tolerance. The cluster automatically detects and quickly recovers from failures. -* Simplifies the complexities of distributed app development by providing a common set of patterns and APIs. -* Enables developers familiar with single-server app development to transition to building resilient, scalable cloud-native services and distributed apps. -* Is sometimes called "Distributed .NET". -* Is the framework of choice when building cloud-native apps. -* Runs anywhere that .NET is supported. This includes hosting on Linux, Windows, and macOS. -* Apps can be deployed to Kubernetes, virtual machines, and PaaS services such as [Azure App Service](/azure/app-service/overview) and [Azure Container Apps](/azure/container-apps/overview). +Here’s what Orleans offers: -## The "Actor Model" +- It’s designed to scale elastically. Add or remove servers, and Orleans adjusts accordingly to maintain fault tolerance and scalability. +- It simplifies distributed app development with a common set of patterns and APIs, making it accessible even for those new to distributed systems. +- It’s cloud-native and runs on platforms where .NET is supported—Linux, Windows, macOS, and more. +- It supports modern deployment options like Kubernetes, Azure App Service, and Azure Container Apps. -Orleans is based on the "actor model". The actor model originated in the early 1970s and is now a core component of Orleans. The actor model is a programming model in which each _actor_ is a lightweight, concurrent, immutable object that encapsulates a piece of state and corresponding behavior. Actors communicate exclusively with each other using asynchronous messages. Orleans notably invented the _Virtual Actor_ abstraction, wherein actors exist perpetually. +Orleans is often referred to as "Distributed .NET" because of its focus on building resilient, scalable cloud-native services. Let’s explore the actor model next. + +## The actor model + +Orleans is based on the actor model. Originating in the early 1970s, the actor model is now a core component of Orleans. In the actor model, each _actor_ is a lightweight, concurrent, immutable object encapsulating a piece of state and corresponding behavior. Actors communicate exclusively using asynchronous messages. Orleans notably invented the _Virtual Actor_ abstraction, where actors exist perpetually. > [!NOTE] > Actors are purely logical entities that _always_ exist, virtually. An actor cannot be explicitly created nor destroyed, and its virtual existence is unaffected by the failure of a server that executes it. Since actors always exist, they are always addressable. -This is a novel approach to building a new generation of distributed apps for the Cloud era. The Orleans programming model tames the complexity inherent to highly parallel distributed apps _without_ restricting capabilities or imposing constraints on the developer. +This novel approach helps build a new generation of distributed apps for the Cloud era. The Orleans programming model tames the complexity inherent in highly parallel distributed apps _without_ restricting capabilities or imposing constraints. For more information, see [Orleans: Virtual Actors](https://www.microsoft.com/research/project/orleans-virtual-actors) via Microsoft Research. A virtual actor is represented as an Orleans grain. -## What are Grains? +## What are grains? The grain is one of several Orleans primitives. In terms of the actor model, a grain is a virtual actor. The fundamental building block in any Orleans application is a *grain*. Grains are entities comprising user-defined identity, behavior, and state. Consider the following visual representation of a grain: :::image type="content" source="media/grain-formulation.svg" lightbox="media/grain-formulation.svg" alt-text="A grain is composed of a stable identity, behavior, and state."::: -Grain identities are user-defined keys that make grains always available for invocation. Grains can be invoked by other grains or by any number of external clients. Each grain is an instance of a class that implements one or more of the following interfaces: +Grain identities are user-defined keys, making grains always available for invocation. Other grains or any number of external clients can invoke grains. Each grain is an instance of a class implementing one or more of the following interfaces: - : Marker interface for grains with `Guid` keys. - : Marker interface for grains with `Int64` keys. @@ -44,93 +43,93 @@ Grain identities are user-defined keys that make grains always available for inv - : Marker interface for grains with compound keys. - : Marker interface for grains with compound keys. -Grains can have volatile or persistent state data that can be stored in any storage system. As such, grains implicitly partition application states, enabling automatic scalability and simplifying recovery from failures. The grain state is kept in memory while the grain is active, leading to lower latency and less load on data stores. +Grains can have volatile or persistent state data stored in any storage system. As such, grains implicitly partition application states, enabling automatic scalability and simplifying recovery from failures. Orleans keeps the grain state in memory while the grain is active, leading to lower latency and less load on data stores. :::image type="content" source="media/grain-lifecycle.svg" lightbox="media/grain-lifecycle.svg" alt-text="The managed lifecycle of an Orleans grain."::: -Instantiation of grains is automatically performed on demand by the Orleans runtime. Grains that aren't used for a while are automatically removed from memory to free up resources. This is possible because of their stable identity, which allows invoking grains whether they're already loaded into memory or not. This also allows for transparent recovery from failure because the caller doesn't need to know on which server a grain is instantiated at any point in time. Grains have a managed lifecycle, with the Orleans runtime responsible for activating/deactivating, and placing/locating grains as needed. This allows the developer to write code as if all grains are always in-memory. +The Orleans runtime automatically instantiates grains on demand. Grains not used for a while are automatically removed from memory to free up resources. This removal is possible due to their stable identity, allowing invocation of grains whether they're loaded into memory or not. This also enables transparent recovery from failure because the caller doesn't need to know on which server a grain is instantiated at any point. Grains have a managed lifecycle, with the Orleans runtime responsible for activating/deactivating and placing/locating grains as needed. This allows writing code as if all grains are always in memory. -## What are Silos? +## What are silos? -A silo is another example of an Orleans primitive. A silo hosts one or more grains. The Orleans runtime is what implements the programming model for applications. +A silo is another example of an Orleans primitive. A silo hosts one or more grains. The Orleans runtime implements the programming model for applications. -Typically, a group of silos runs as a cluster for scalability and fault tolerance. When run as a cluster, silos coordinate with each other to distribute work and detect and recover from failures. The runtime enables grains hosted in the cluster to communicate with each other as if they are within a single process. To help visualize the relationship between clusters, silos and grains, consider the following diagram: +Typically, a group of silos runs as a cluster for scalability and fault tolerance. When run as a cluster, silos coordinate to distribute work and detect and recover from failures. The runtime enables grains hosted in the cluster to communicate as if they are within a single process. To help visualize the relationship between clusters, silos, and grains, consider the following diagram: :::image type="content" source="media/cluster-silo-grain-relationship.svg" lightbox="media/cluster-silo-grain-relationship.svg" alt-text="A cluster has one or more silos, and a silo has one or more grains."::: -The preceding diagram shows the relationship between clusters, silos, and grains. You can have any number of clusters, each cluster has one or more silos, and each silo has one or more grains. +The preceding diagram shows the relationship between clusters, silos, and grains. There can be any number of clusters, each cluster has one or more silos, and each silo has one or more grains. -In addition to the core programming model, silos provide grains with a set of runtime services such as timers, reminders (persistent timers), persistence, transactions, streams, and more. For more information, see [What can I do with Orleans?](#what-can-i-do-with-orleans). +In addition to the core programming model, silos provide grains with runtime services such as timers, reminders (persistent timers), persistence, transactions, streams, and more. For more information, see [What can be done with Orleans?](#what-can-be-done-with-orleans). Web apps and other external clients call grains in the cluster using the client library, which automatically manages network communication. Clients can also be co-hosted in the same process with silos for simplicity. -## What can I do with Orleans? +## What can be done with Orleans? -Orleans is a framework for building cloud-native apps and should be considered whenever you're building .NET apps that would eventually need to scale. There are seemingly endless ways to use Orleans, but the following are some of the most common ways; Gaming, Banking, Chat apps, GPS tracking, Stock trading, Shopping carts, Voting apps, and more. Orleans is used by Microsoft in Azure, Xbox, Skype, Halo, PlayFab, Gears of War, and many other internal services. Orleans has many features that make it easy to use for a variety of applications. +Orleans is a framework for building cloud-native apps and should be considered when building .NET apps that might eventually need to scale. There are seemingly endless ways to use Orleans, but the following are some of the most common: Gaming, Banking, Chat apps, GPS tracking, Stock trading, Shopping carts, Voting apps, and more. Microsoft uses Orleans in Azure, Xbox, Skype, Halo, PlayFab, Gears of War, and many other internal services. Orleans has many features making it easy to use for various applications. ### Persistence -Orleans provides a simple persistence model that ensures the state is available before processing a request, and that its consistency is maintained. Grains can have multiple named persistent data objects. For example, there might be one called "profile" for a user's profile and one called "inventory" for their inventory. This state can be stored in any storage system. +Orleans provides a simple persistence model ensuring state availability before processing a request and maintaining consistency. Grains can have multiple named persistent data objects. For example, one might be called "profile" for a user's profile and another "inventory" for their inventory. This state can be stored in any storage system. -While a grain is running, the state is kept in memory so that read requests can be served without accessing storage. When the grain updates its state, calling ensures that the backing store is updated for durability and consistency. +While a grain runs, Orleans keeps the state in memory to serve read requests without accessing storage. When the grain updates its state, calling ensures the backing store updates for durability and consistency. For more information, see [Grain persistence](grains/grain-persistence/index.md). ### Timers and reminders -Reminders are a durable scheduling mechanism for grains. They can be used to ensure that some action is completed at a future point even if the grain isn't currently activated at that time. Timers are the non-durable counterpart to reminders and can be used for high-frequency events, which don't require reliability. +Reminders are a durable scheduling mechanism for grains. Use them to ensure an action completes at a future point, even if the grain isn't currently activated. Timers are the non-durable counterpart to reminders and can be used for high-frequency events not requiring reliability. For more information, see [Timers and reminders](grains/timers-and-reminders.md). ### Flexible grain placement -When a grain is activated in Orleans, the runtime decides which server (silo) to activate that grain on. This is called grain placement. +When a grain activates in Orleans, the runtime decides which server (silo) to activate it on. This process is called grain placement. -The placement process in Orleans is fully configurable. Developers can choose from a set of out-of-the-box placement policies such as random, prefer-local, and load-based, or custom logic can be configured. This allows for full flexibility in deciding where grains are created. For example, grains can be placed on a server close to resources that they need to operate against or other grains with which they communicate. +The placement process in Orleans is fully configurable. Choose from out-of-the-box placement policies such as random, prefer-local, and load-based, or configure custom logic. This allows full flexibility in deciding where grains are created. For example, place grains on a server close to resources they need to operate against or other grains they communicate with. For more information, see [Grain placement](grains/grain-placement.md). ### Grain versioning and heterogeneous clusters -Upgrading production systems in a manner that safely accounts for changes can be challenging, particularly in stateful systems. To account for this, grain interfaces in Orleans can be versioned. +Upgrading production systems safely accounting for changes can be challenging, particularly in stateful systems. To account for this, grain interfaces can be versioned in Orleans. -The cluster maintains a mapping of which grain implementations are available on which silos in the cluster and the versions of those implementations. This version of the information is used by the runtime in conjunction with placement strategies to make placement decisions when routing calls to grains. In addition, to safely update a versioned grain, this also enables heterogeneous clusters, where different silos have different sets of grain implementations available. +The cluster maintains a mapping of which grain implementations are available on which silos and their versions. The runtime uses this version information with placement strategies to make placement decisions when routing calls to grains. Besides safely updating a versioned grain, this also enables heterogeneous clusters where different silos have different sets of available grain implementations. For more information, see [Grain Versioning](grains/grain-versioning/grain-versioning.md). ### Stateless workers -Stateless workers are specially marked grains that don't have any associated state and can be activated on multiple silos simultaneously. This enables increased parallelism for stateless functions. +Stateless workers are specially marked grains without associated state that can activate on multiple silos simultaneously. This enables increased parallelism for stateless functions. For more information, see [stateless worker grains](grains/stateless-worker-grains.md). ### Grain call filters -A [grain call filter](grains/interceptors.md) is logic that's common to many grains. Orleans supports filters for both incoming and outgoing calls. Filters for authorization, logging and telemetry, and error handling are all considered common. +A [grain call filter](grains/interceptors.md) is logic common to many grains. Orleans supports filters for both incoming and outgoing calls. Common uses include authorization, logging and telemetry, and error handling. ### Request context -Metadata and other information can be passed with a series of requests using the [request context](grains/request-context.md). Request context can be used for holding distributed tracing information or any other user-defined values. +Pass metadata and other information with a series of requests using the [request context](grains/request-context.md). Use the request context for holding distributed tracing information or any other defined values. ### Distributed ACID transactions -In addition to the simple persistence model described above, grains can have a *transactional state*. Multiple grains can participate in [ACID](/windows/win32/cossdk/acid-properties) transactions together regardless of where their state is ultimately stored. Transactions in Orleans are distributed and decentralized (there is no central transaction manager or transaction coordinator) and have [serializable isolation](https://en.wikipedia.org/wiki/Isolation_(database_systems)#Isolation_levels). +Besides the simple persistence model described above, grains can have *transactional state*. Multiple grains can participate in [ACID](/windows/win32/cossdk/acid-properties) transactions together, regardless of where their state is ultimately stored. Transactions in Orleans are distributed and decentralized (meaning no central transaction manager or coordinator) and have [serializable isolation](https://en.wikipedia.org/wiki/Isolation_(database_systems)#Isolation_levels). For more information on transactions, see [Transactions](grains/transactions.md). ### Streams -Streams help developers to process a series of data items in near-real-time. Orleans streams are *managed*; streams don't need to be created or registered before a grain or client publishes, or subscribes to a stream. This allows for greater decoupling of stream producers and consumers from each other and the infrastructure. +Streams help process a series of data items in near-real-time. Orleans streams are *managed*; streams don't need creating or registering before a grain or client publishes or subscribes. This allows greater decoupling of stream producers and consumers from each other and the infrastructure. -Stream processing is reliable: grains can store checkpoints (cursors) and reset to a stored checkpoint during activation or at any subsequent time. Streams support batch delivery of messages to consumers to improve efficiency and recovery performance. +Stream processing is reliable: grains can store checkpoints (cursors) and reset to a stored checkpoint during activation or any subsequent time. Streams support batch delivery of messages to consumers to improve efficiency and recovery performance. Streams are backed by queueing services such as Azure Event Hubs, Amazon Kinesis, and others. -An arbitrary number of streams can be multiplexed onto a smaller number of queues and the responsibility for processing these queues is balanced evenly across the cluster. +An arbitrary number of streams can be multiplexed onto a smaller number of queues, and the responsibility for processing these queues is balanced evenly across the cluster. ## Introduction to Orleans video -If you're interested in a video introduction to Orleans, check out the following video: +For a video introduction to Orleans, check out the following video: > [!VIDEO https://aka.ms/docs/player?show=reactor&ep=an-introduction-to-orleans] diff --git a/docs/orleans/quickstarts/build-your-first-orleans-app.md b/docs/orleans/quickstarts/build-your-first-orleans-app.md index 73481da2a57da..006b858063ce0 100644 --- a/docs/orleans/quickstarts/build-your-first-orleans-app.md +++ b/docs/orleans/quickstarts/build-your-first-orleans-app.md @@ -12,10 +12,10 @@ In this quickstart, you use Orleans and ASP.NET Core 8.0 Minimal APIs to build a At the end of the quickstart, you have an app that creates and handles redirects using short, friendly URLs. You learn how to: -* Add Orleans to an ASP.NET Core app -* Work with grains and silos -* Configure state management -* Integrate Orleans with API endpoints +- Add Orleans to an ASP.NET Core app +- Work with grains and silos +- Configure state management +- Integrate Orleans with API endpoints ## Prerequisites @@ -56,9 +56,11 @@ At the end of the quickstart, you have an app that creates and handles redirects The `dotnet new` command creates a new Minimal API project in the *OrleansURLShortener* folder. The `code` command opens the *OrleansURLShortener* folder in the current instance of Visual Studio Code. - Visual Studio Code displays a dialog box that asks **Do you trust the authors of the files in this folder**. Select: - * The checkbox **trust the authors of all files in the parent folder** - * **Yes, I trust the authors** (because dotnet generated the files). + Visual Studio Code displays a dialog box that asks **Do you trust the authors of the files in this folder**. + Select: + + - The checkbox **trust the authors of all files in the parent folder** + - **Yes, I trust the authors** (because dotnet generated the files). --- diff --git a/docs/orleans/quickstarts/deploy-scale-orleans-on-azure.md b/docs/orleans/quickstarts/deploy-scale-orleans-on-azure.md index d2402eea7a8b6..cfbf1762d130b 100644 --- a/docs/orleans/quickstarts/deploy-scale-orleans-on-azure.md +++ b/docs/orleans/quickstarts/deploy-scale-orleans-on-azure.md @@ -33,52 +33,52 @@ The sample application is available as an Azure Developer CLI template. Through 1. Authenticate to the Azure Developer CLI using `azd auth login`. Follow the steps specified by the tool to authenticate to the CLI using your preferred Azure credentials. - ```azuredeveloper - azd auth login - ``` + ```azuredeveloper + azd auth login + ``` 1. Get the sample application using the AZD template `orleans-url-shortener` and the `azd init` command. - ```azuredeveloper - azd init --template orleans-url-shortener - ``` + ```azuredeveloper + azd init --template orleans-url-shortener + ``` 1. During initialization, configure a unique environment name. - > [!TIP] - > The environment name will also be used as the target resource group name. For this quickstart, consider using `msdocs-orleans-url-shortener`. + > [!TIP] + > The environment name will also be used as the target resource group name. For this quickstart, consider using `msdocs-orleans-url-shortener`. 1. Deploy the Azure Cosmos DB for NoSQL account using `azd up`. The Bicep templates also deploy a sample web application. - ```azuredeveloper - azd up - ``` + ```azuredeveloper + azd up + ``` 1. During the provisioning process, select your subscription and desired location. Wait for the provisioning and deployment process to complete. The process can take **approximately five minutes**. 1. Once the provisioning of your Azure resources is done, a URL to the running web application is included in the output. - ```output - Deploying services (azd deploy) + ```output + Deploying services (azd deploy) - (✓) Done: Deploying service web - - Endpoint: + (✓) Done: Deploying service web + - Endpoint: - SUCCESS: Your application was provisioned and deployed to Azure in 5 minutes 0 seconds. - ``` + SUCCESS: Your application was provisioned and deployed to Azure in 5 minutes 0 seconds. + ``` 1. Use the URL in the console to navigate to your web application in the browser. - :::image type="content" source="media/deploy-scale-Orleans-on-azure/web-application.png" alt-text="Screenshot of the running URL shortener web application."::: + :::image type="content" source="media/deploy-scale-Orleans-on-azure/web-application.png" alt-text="Screenshot of the running URL shortener web application."::: 1. In the browser address bar, test the `shorten` endpoint by adding a URL path such as `/shorten?url=https://www.microsoft.com`. The page should reload and provide a new URL with a shortened path at the end. Copy the new URL to your clipboard. - ```output - { - "original": "https://www.microsoft.com", - "shortened": "http://...azurecontainerapps.io:/go/" - } - ``` + ```output + { + "original": "https://www.microsoft.com", + "shortened": "http://...azurecontainerapps.io:/go/" + } + ``` 1. Paste the shortened URL into the address bar and press enter. The page should reload and redirect you to the URL you specified. @@ -90,9 +90,9 @@ The original deployment only deployed the minimal services necessary to host the 1. Using the terminal, run `azd env set` to configure the `DEPLOY_AZURE_TABLE_STORAGE` environment variable to enable deployment of Azure Cosmos DB for NoSQL. - ```azuredeveloper - azd env set DEPLOY_AZURE_TABLE_STORAGE true - ``` + ```azuredeveloper + azd env set DEPLOY_AZURE_TABLE_STORAGE true + ``` ::: zone-end @@ -100,20 +100,20 @@ The original deployment only deployed the minimal services necessary to host the 1. Using the terminal, run `azd env set` to configure the `DEPLOY_AZURE_COSMOS_DB_NOSQL` environment variable to enable deployment of Azure Cosmos DB for NoSQL. - ```azuredeveloper - azd env set DEPLOY_AZURE_COSMOS_DB_NOSQL true - ``` + ```azuredeveloper + azd env set DEPLOY_AZURE_COSMOS_DB_NOSQL true + ``` ::: zone-end 1. Run `azd provision` to redeploy your application architecture with the new configuration. Wait for the provisioning process to complete. The process can take **approximately two minutes**. - ```azuredeveloper - azd provision - ``` + ```azuredeveloper + azd provision + ``` - > [!TIP] - > Alternatively, you can run `azd up` again which will both provision your architecture and redeploy your application. + > [!TIP] + > Alternatively, you can run `azd up` again which will both provision your architecture and redeploy your application. ## Install NuGet packages @@ -123,15 +123,15 @@ Prior to using the grain, you must install the corresponding `Microsoft.Orleans. 1. Change your current working directory to _./src/web/_. - ```bash - cd ./src/web - ``` + ```bash + cd ./src/web + ``` 1. Import the `Azure.Identity` package from NuGet: - ```dotnetcli - dotnet add package Azure.Identity --version 1.* - ``` + ```dotnetcli + dotnet add package Azure.Identity --version 1.* + ``` 1. Import the `Microsoft.Orleans.Clustering.AzureStorage` and `Microsoft.Orleans.Persistence.AzureStorage` packages. @@ -140,10 +140,10 @@ Prior to using the grain, you must install the corresponding `Microsoft.Orleans. | **Clustering** | `Microsoft.Orleans.Clustering.AzureStorage` | | **Persistence** | `Microsoft.Orleans.Persistence.AzureStorage` | - ```dotnetcli - dotnet add package Microsoft.Orleans.Clustering.AzureStorage --version 8.* - dotnet add package Microsoft.Orleans.Persistence.AzureStorage --version 8.* - ``` + ```dotnetcli + dotnet add package Microsoft.Orleans.Clustering.AzureStorage --version 8.* + dotnet add package Microsoft.Orleans.Persistence.AzureStorage --version 8.* + ``` ::: zone-end @@ -151,9 +151,9 @@ Prior to using the grain, you must install the corresponding `Microsoft.Orleans. 1. Import the `Azure.Identity` package from NuGet: - ```dotnetcli - dotnet add package Azure.Identity --version 1.* - ``` + ```dotnetcli + dotnet add package Azure.Identity --version 1.* + ``` 1. Import the `Microsoft.Orleans.Clustering.Cosmos` and `Microsoft.Orleans.Persistence.Cosmos` packages. @@ -162,10 +162,10 @@ Prior to using the grain, you must install the corresponding `Microsoft.Orleans. | **Clustering** | `Microsoft.Orleans.Clustering.Cosmos` | | **Persistence** | `Microsoft.Orleans.Persistence.Cosmos` | - ```dotnetcli - dotnet add package Microsoft.Orleans.Clustering.Cosmos --version 8.* - dotnet add package Microsoft.Orleans.Persistence.Cosmos --version 8.* - ``` + ```dotnetcli + dotnet add package Microsoft.Orleans.Clustering.Cosmos --version 8.* + dotnet add package Microsoft.Orleans.Persistence.Cosmos --version 8.* + ``` ::: zone-end @@ -175,32 +175,32 @@ The sample app is currently configured to create a localhost cluster and persist 1. Add the following `using` directives: - ```csharp - using Azure.Identity; - using Orleans.Configuration; - ``` + ```csharp + using Azure.Identity; + using Orleans.Configuration; + ``` 1. Find and remove the current `builder` configuration code in the _src/web/Program.cs_ file. - ```csharp - builder.Host.UseOrleans(static siloBuilder => - { - siloBuilder - .UseLocalhostClustering() - .AddMemoryGrainStorage("urls"); - }); - ``` + ```csharp + builder.Host.UseOrleans(static siloBuilder => + { + siloBuilder + .UseLocalhostClustering() + .AddMemoryGrainStorage("urls"); + }); + ``` ::: zone pivot="azure-storage" 1. Replace the `builder` configuration with the example here, which implements these key concepts: - - A conditional environment check is added to ensure the app runs properly in both local development and Azure hosted scenarios. - - The `UseAzureStorageClustering` method configures the Orleans cluster to use Azure Table Storage and authenticates using the class. - - Use the `Configure` method to assign IDs for the Orleans cluster. - - The `ClusterID` is a unique ID for the cluster that allows clients and silos to talk to one another. - - The `ClusterID` can change across deployments. - - The `ServiceID` is a unique ID for the application that is used internally by Orleans and should remain consistent across deployments. + - A conditional environment check is added to ensure the app runs properly in both local development and Azure hosted scenarios. + - The `UseAzureStorageClustering` method configures the Orleans cluster to use Azure Table Storage and authenticates using the class. + - Use the `Configure` method to assign IDs for the Orleans cluster. + - The `ClusterID` is a unique ID for the cluster that allows clients and silos to talk to one another. + - The `ClusterID` can change across deployments. + - The `ServiceID` is a unique ID for the application that is used internally by Orleans and should remain consistent across deployments. ```csharp if (builder.Environment.IsDevelopment()) @@ -243,58 +243,58 @@ The sample app is currently configured to create a localhost cluster and persist 1. Replace the `builder` configuration with the example here, which implements these key concepts: - - A conditional environment check is added to ensure the app runs properly in both local development and Azure hosted scenarios. - - The `UseCosmosClustering` method configures the Orleans cluster to use Azure Cosmos DB for NoSQL and authenticates using the class. - - Use the `Configure` method to assign IDs for the Orleans cluster. - - The `ClusterID` is a unique ID for the cluster that allows clients and silos to talk to one another. - - The `ClusterID` can change across deployments. - - The `ServiceID` is a unique ID for the application that is used internally by Orleans and should remain consistent across deployments. - - ```csharp - if (builder.Environment.IsDevelopment()) - { - builder.Host.UseOrleans(static siloBuilder => - { - siloBuilder - .UseLocalhostClustering() - .AddMemoryGrainStorage("urls"); - }); - } - else - { - builder.Host.UseOrleans(siloBuilder => - { - var endpoint = builder.Configuration["AZURE_COSMOS_DB_NOSQL_ENDPOINT"]!; - var credential = new DefaultAzureCredential(); - - siloBuilder - .UseCosmosClustering(options => - { - options.ConfigureCosmosClient(endpoint, credential); - }) - .AddCosmosGrainStorage(name: "urls", options => - { - options.ConfigureCosmosClient(endpoint, credential); - }) - .Configure(options => - { - options.ClusterId = "url-shortener"; - options.ServiceId = "urls"; - }); - }); - } - ``` + - A conditional environment check is added to ensure the app runs properly in both local development and Azure hosted scenarios. + - The `UseCosmosClustering` method configures the Orleans cluster to use Azure Cosmos DB for NoSQL and authenticates using the class. + - Use the `Configure` method to assign IDs for the Orleans cluster. + - The `ClusterID` is a unique ID for the cluster that allows clients and silos to talk to one another. + - The `ClusterID` can change across deployments. + - The `ServiceID` is a unique ID for the application that is used internally by Orleans and should remain consistent across deployments. + + ```csharp + if (builder.Environment.IsDevelopment()) + { + builder.Host.UseOrleans(static siloBuilder => + { + siloBuilder + .UseLocalhostClustering() + .AddMemoryGrainStorage("urls"); + }); + } + else + { + builder.Host.UseOrleans(siloBuilder => + { + var endpoint = builder.Configuration["AZURE_COSMOS_DB_NOSQL_ENDPOINT"]!; + var credential = new DefaultAzureCredential(); + + siloBuilder + .UseCosmosClustering(options => + { + options.ConfigureCosmosClient(endpoint, credential); + }) + .AddCosmosGrainStorage(name: "urls", options => + { + options.ConfigureCosmosClient(endpoint, credential); + }) + .Configure(options => + { + options.ClusterId = "url-shortener"; + options.ServiceId = "urls"; + }); + }); + } + ``` ::: zone-end 1. Run `azd deploy` to redeploy your application code as a Docker container. Wait for the deployment process to complete. The process can take **approximately one minute**. - ```azuredeveloper - azd deploy - ``` + ```azuredeveloper + azd deploy + ``` - > [!TIP] - > Alternatively, you can run `azd up` again which will both provision your architecture and redeploy your application. + > [!TIP] + > Alternatively, you can run `azd up` again which will both provision your architecture and redeploy your application. ## Verify the app's behavior @@ -302,12 +302,12 @@ Validate that your updated code works by using the deployed application again an 1. In the browser address bar, test the `shorten` endpoint again by adding a URL path such as `/shorten?url=https://learn.microsoft.com/dotnet/orleans`. The page should reload and provide a new URL with a shortened path at the end. Copy the new URL to your clipboard. - ```output - { - "original": "https://learn.microsoft.com/dotnet/orleans", - "shortened": "http://...azurecontainerapps.io:/go/" - } - ``` + ```output + { + "original": "https://learn.microsoft.com/dotnet/orleans", + "shortened": "http://...azurecontainerapps.io:/go/" + } + ``` 1. Paste the shortened URL into the address bar and press enter. The page should reload and redirect you to the URL you specified. @@ -315,8 +315,8 @@ Optionally, you can verify that the cluster and state data is stored as expected 1. In the Azure portal, navigate to the resource group that was deployed in this quickstart. - > [!IMPORTANT] - > The environment name specified earlier in this quickstart is also the target resource group name. + > [!IMPORTANT] + > The environment name specified earlier in this quickstart is also the target resource group name. ::: zone pivot="azure-storage" @@ -326,13 +326,12 @@ Optionally, you can verify that the cluster and state data is stored as expected 1. Expand the **Tables** navigation item to discover two tables created by Orleans: - - **OrleansGrainState**: This table stores the persistent state grain data used by the application to handle the URL redirects. - - - **OrleansSiloInstances**: This table tracks essential silo data for the Orleans cluster. + - **OrleansGrainState**: This table stores the persistent state grain data used by the application to handle the URL redirects. + - **OrleansSiloInstances**: This table tracks essential silo data for the Orleans cluster. 1. Select the **OrleansGrainState** table. The table holds a row entry for every URL redirect persisted by the app during your testing. - :::image type="content" source="media/deploy-scale-Orleans-on-azure/storage-table-entities.png" alt-text="A screenshot showing Orleans data in Azure Table Storage."::: + :::image type="content" source="media/deploy-scale-Orleans-on-azure/storage-table-entities.png" alt-text="A screenshot showing Orleans data in Azure Table Storage."::: ::: zone-end @@ -344,9 +343,9 @@ Optionally, you can verify that the cluster and state data is stored as expected 1. Observe the following containers you created earlier in this guide: - - **OrleansStorage**: This table stores the persistent state grain data used by the application to handle the URL redirects. + - **OrleansStorage**: This table stores the persistent state grain data used by the application to handle the URL redirects. - - **OrleansCluster**: This table tracks essential silo data for the Orleans cluster. + - **OrleansCluster**: This table tracks essential silo data for the Orleans cluster. ::: zone-end @@ -366,7 +365,7 @@ Orleans is designed for distributed applications. Even an app as simple as the U 1. Select **Create** to deploy the new revision. - :::image type="content" source="media/deploy-scale-Orleans-on-azure/scale-containers.png" alt-text="A screenshot showing how to scale the Azure Container Apps app."::: + :::image type="content" source="media/deploy-scale-Orleans-on-azure/scale-containers.png" alt-text="A screenshot showing how to scale the Azure Container Apps app."::: 1. After the deployment is finished, repeat the testing steps from the previous section. The app continues to work as expected across several instances and can now handle a higher number of requests. diff --git a/docs/orleans/resources/best-practices.md b/docs/orleans/resources/best-practices.md index 0ef40d7e25ecb..e68e45d483449 100644 --- a/docs/orleans/resources/best-practices.md +++ b/docs/orleans/resources/best-practices.md @@ -49,7 +49,7 @@ Orleans is not the best fit when: - Direct memory use is significantly less expensive than message passing. - Highly chatty grains may be better combined as a single grain. - Complexity/Size of arguments and serialization needs to be considered. - - Deserializing twice may be more expensive than resending a binary message. + - Deserializing twice may be more expensive than resending a binary message. - Avoid bottleneck grains. - Single coordinator/Registry/Monitor. - Do staged aggregation if required. @@ -60,9 +60,9 @@ Orleans is not the best fit when: - [await](/dotnet/csharp/programming-guide/concepts/async/) is the best syntax to use when composing async operations. - Common Scenarios: - Return a concrete value: - - `return Task.FromResult(value);` + - `return Task.FromResult(value);` - Return a `Task` of the same type: - - `return foo.Bar();` + - `return foo.Bar();` - `await` a `Task` and continue execution: ```csharp @@ -97,10 +97,10 @@ Orleans is not the best fit when: - Example: Performs well with staged aggregation within local silo first. - Grains are non-reentrant by default. - Deadlock can occur due to call cycles. - - Examples: - - The grain calls itself. - - Grain A calls B which calls C which in turn is calling A (A -> B -> C -> A). - - Grain A calls Grain B as Grain B is calling Grain A (A -> B -> A). + - Examples: + - The grain calls itself. + - Grain A calls B which calls C which in turn is calling A (A -> B -> C -> A). + - Grain A calls Grain B as Grain B is calling Grain A (A -> B -> A). - Timeouts are used to automatically break deadlocks. - can be used to allow the grain class reentrant. - Reentrant is still single-threaded however, it may interleave (divide processing/memory between tasks). @@ -122,11 +122,11 @@ extensible storage functionality. - Typically, grains call `State.WriteStateAsync()` at the end of grain method to return the Write promise. - The Storage provider *could* try to batch Writes that may increase efficiency, but behavioral contracts and configurations are orthogonal (independent) to the storage API used by the grain. - A **timer** is an alternative method to write updates periodically. - - The timer allows the application to determine the amount of "eventual consistency"/statelessness allowed. - - Timing (immediate/none/minutes) can also be controlled as to when to update. + - The timer allows the application to determine the amount of "eventual consistency"/statelessness allowed. + - Timing (immediate/none/minutes) can also be controlled as to when to update. - decorated classes, like other grain classes, can only be associated with one storage provider. - - [StorageProvider(ProviderName = "name")](xref:Orleans.Providers.StorageProviderAttribute) attribute associates the grain class with a particular provider. - - `` will need to be added to the silo config file which should also include the corresponding "name" from `[StorageProvider(ProviderName="name")]`. + - [StorageProvider(ProviderName = "name")](xref:Orleans.Providers.StorageProviderAttribute) attribute associates the grain class with a particular provider. + - `` will need to be added to the silo config file which should also include the corresponding "name" from `[StorageProvider(ProviderName="name")]`. ## Storage providers @@ -147,7 +147,7 @@ Built-in storage providers: - TraceOverride Verbose3 will log much more information about storage operations. - Update silo config file. - - LogPrefix="Storage" for all providers, or specific type using "Storage.Memory" / "Storage.Azure" / "Storage.Shard". + - LogPrefix="Storage" for all providers, or specific type using "Storage.Memory" / "Storage.Azure" / "Storage.Shard". How to deal with storage operation failures: @@ -168,7 +168,7 @@ External changing data: - Grains can re-read the current state data from storage by using `State.ReadStateAsync()`. - A timer can also be used to re-read data from storage periodically as well. - The functional requirements could be based on a suitable "staleness" of the information. - - Example: Content cache grain. + - Example: Content cache grain. - Adding and removing fields. - The storage provider will determine the effects of adding and removing additional fields from their persisted state. @@ -191,7 +191,7 @@ Writing custom providers: - Grains can timeout. A retry solution such as Polly can assist with retries. - Orleans provides a message delivery guarantee where each message is delivered at-most-once. - It is the responsibility of the caller to [retry](https://github.com/App-vNext/Polly/wiki/Retry) any failed calls if needed. - - Common practice is to retry from end-to-end from the client/front end. + - Common practice is to retry from end-to-end from the client/front end. ## Deployment and production management diff --git a/docs/orleans/resources/nuget-packages.md b/docs/orleans/resources/nuget-packages.md index 398fcda329102..59c7c12e79f40 100644 --- a/docs/orleans/resources/nuget-packages.md +++ b/docs/orleans/resources/nuget-packages.md @@ -131,7 +131,7 @@ Add it to your projects that define grain interfaces and classes. ### Orleans build-time code generation -* [Microsoft.Orleans.OrleansCodeGenerator.Build](https://www.nuget.org/packages/Microsoft.Orleans.OrleansCodeGenerator.Build/). +- [Microsoft.Orleans.OrleansCodeGenerator.Build](https://www.nuget.org/packages/Microsoft.Orleans.OrleansCodeGenerator.Build/). ```powershell Install-Package Microsoft.Orleans.OrleansCodeGenerator.Build @@ -140,7 +140,7 @@ Add it to your projects that define grain interfaces and classes. Appeared in Orleans 1.2.0. Build time support for grain interfaces and implementation projects. Add it to your grain interfaces and implementation projects to enable code generation of grain references and serializers. -* [Microsoft.Orleans.CodeGenerator.MSBuild](https://www.nuget.org/packages/Microsoft.Orleans.CodeGenerator.MSBuild/). +- [Microsoft.Orleans.CodeGenerator.MSBuild](https://www.nuget.org/packages/Microsoft.Orleans.CodeGenerator.MSBuild/). ```powershell Install-Package Microsoft.Orleans.CodeGenerator.MSBuild @@ -156,10 +156,10 @@ Install-Package Microsoft.Orleans.Server A meta-package for easily building and starting a silo. Includes the following packages: -* `Microsoft.Orleans.Core.Abstractions` -* `Microsoft.Orleans.Core` -* `Microsoft.Orleans.OrleansRuntime` -* `Microsoft.Orleans.OrleansProviders` +- `Microsoft.Orleans.Core.Abstractions` +- `Microsoft.Orleans.Core` +- `Microsoft.Orleans.OrleansRuntime` +- `Microsoft.Orleans.OrleansProviders` ### [Orleans Client Libraries](https://www.nuget.org/packages/Microsoft.Orleans.Client/) @@ -169,9 +169,9 @@ Install-Package Microsoft.Orleans.Client A meta-package for easily building and starting an Orleans client (frontend). Includes the following packages: -* `Microsoft.Orleans.Core.Abstractions` -* `Microsoft.Orleans.Core` -* `Microsoft.Orleans.OrleansProviders` +- `Microsoft.Orleans.Core.Abstractions` +- `Microsoft.Orleans.Core` +- `Microsoft.Orleans.OrleansProviders` ### [Orleans Core Library](https://www.nuget.org/packages/Microsoft.Orleans.Core/) diff --git a/docs/orleans/resources/orleans-architecture-principles-and-approach.md b/docs/orleans/resources/orleans-architecture-principles-and-approach.md index c8e836c6b66dc..0c9a52406c3d4 100644 --- a/docs/orleans/resources/orleans-architecture-principles-and-approach.md +++ b/docs/orleans/resources/orleans-architecture-principles-and-approach.md @@ -10,28 +10,28 @@ Orleans is an open-source project, and it's important to be clear about the goal The goal for the Orleans project was to produce a framework that would allow mainstream developers to easily build scalable distributed (cloud) applications. To break this down a bit: -* The target audience shouldn't exclude programmers who haven't done distributed systems development. We want to enable all developers, whether cloud experts or cloud beginners, to focus on their application logic and features—which is to say, what provides business value—rather than on generic distributed systems issues. +- The target audience shouldn't exclude programmers who haven't done distributed systems development. We want to enable all developers, whether cloud experts or cloud beginners, to focus on their application logic and features—which is to say, what provides business value—rather than on generic distributed systems issues. -* The goal is to allow mainstream developers to build cloud applications easily. *Easily* means that they shouldn't have to think about distribution any more than is required. *Easily* also means that Orleans should present as familiar a facade to the developer as possible; in a .NET context, that means C# objects and interfaces. +- The goal is to allow mainstream developers to build cloud applications easily. *Easily* means that they shouldn't have to think about distribution any more than is required. *Easily* also means that Orleans should present as familiar a facade to the developer as possible; in a .NET context, that means C# objects and interfaces. -* Those applications should be _scalable by default_. Since the target users aren't necessarily distributed systems experts, we want to provide them with a framework that leads them to build scalable applications without explicitly thinking about it. This means that the framework has to make a lot of decisions for them to guarantee an acceptable degree of scalability, even if that means that the scalability isn't optimal for every application. +- Those applications should be _scalable by default_. Since the target users aren't necessarily distributed systems experts, we want to provide them with a framework that leads them to build scalable applications without explicitly thinking about it. This means that the framework has to make a lot of decisions for them to guarantee an acceptable degree of scalability, even if that means that the scalability isn't optimal for every application. We supplemented this goal with a set of architectural principles: -* We're focused on the 80% case. There are certain applications that Orleans isn't appropriate for; that's OK. There are applications that Orleans is a reasonable fit for, but where you can get somewhat better performance by a bunch of hand-tuning that Orleans doesn't allow; that's OK too. The 80% that Orleans fits well and performs well enough on covers a lot of interesting applications, and we'd rather do a great job on 80% than a lousy job on 99%. +- We're focused on the 80% case. There are certain applications that Orleans isn't appropriate for; that's OK. There are applications that Orleans is a reasonable fit for, but where you can get somewhat better performance by a bunch of hand-tuning that Orleans doesn't allow; that's OK too. The 80% that Orleans fits well and performs well enough on covers a lot of interesting applications, and we'd rather do a great job on 80% than a lousy job on 99%. -* Scalability is paramount. We'll trade off raw performance if that gets us better scaling. +- Scalability is paramount. We'll trade off raw performance if that gets us better scaling. -* Availability is paramount. A cloud application should be like a utility: always there when you want it. +- Availability is paramount. A cloud application should be like a utility: always there when you want it. -* Detect and fix problems, don't assume you can 100% prevent them. At cloud scale, bad things happen often, and even impossible bad things happen, just less often. This has led us to what is often termed "recovery-oriented computing", rather than trying to be fault-tolerant. Experience has shown that fault tolerance is fragile and often illusory. Even mathematically proven protocols don't protect against random bit flips in memory or disk controllers that fail while reporting success—both examples that have occurred in the real world. +- Detect and fix problems, don't assume you can 100% prevent them. At cloud scale, bad things happen often, and even impossible bad things happen, just less often. This has led us to what is often termed "recovery-oriented computing", rather than trying to be fault-tolerant. Experience has shown that fault tolerance is fragile and often illusory. Even mathematically proven protocols don't protect against random bit flips in memory or disk controllers that fail while reporting success—both examples that have occurred in the real world. The previous principles have led us to certain practices: -* API-first design: if we don't know how we're going to expose a feature to the developer, then we don't build it. Of course, the best way is for a feature to have no developer exposure at all. +- API-first design: if we don't know how we're going to expose a feature to the developer, then we don't build it. Of course, the best way is for a feature to have no developer exposure at all. -* Make it easy to do the right thing: keep things as simple as possible (but no simpler), don't provide a hammer if a screwdriver is the right tool. As one of our early adopters put it, we try to help our customers "fall into the pit of success". If there is a standard pattern that will work well for 80% of the applications out there, then don't worry about enabling every possible alternative. Orleans' embrace of asynchrony is a good example of this. +- Make it easy to do the right thing: keep things as simple as possible (but no simpler), don't provide a hammer if a screwdriver is the right tool. As one of our early adopters put it, we try to help our customers "fall into the pit of success". If there is a standard pattern that will work well for 80% of the applications out there, then don't worry about enabling every possible alternative. Orleans' embrace of asynchrony is a good example of this. -* Make it easy for developers to extend the framework without breaking it. Custom serialization and persistence providers are a couple of examples of this. A custom task scheduling extension would be an anti-example. +- Make it easy for developers to extend the framework without breaking it. Custom serialization and persistence providers are a couple of examples of this. A custom task scheduling extension would be an anti-example. -* Follow the principle of least surprise: as much as possible, things should be as familiar, but everything should behave the way it looks. +- Follow the principle of least surprise: as much as possible, things should be as familiar, but everything should behave the way it looks. diff --git a/docs/orleans/streaming/index.md b/docs/orleans/streaming/index.md index 62bb631e6ead0..6912ec6320953 100644 --- a/docs/orleans/streaming/index.md +++ b/docs/orleans/streaming/index.md @@ -75,9 +75,9 @@ Orleans provides several stream provider implementations: Orleans currently includes several provider implementations: -* **Simple Message** (SMS), which uses direct grain calls and no backing storage system, -* **Azure Queues**, which uses Azure Storage Queues to store messages, and -* **Azure EventHubs**, which uses Azure EventHubs +- **Simple Message** (SMS), which uses direct grain calls and no backing storage system, +- **Azure Queues**, which uses Azure Storage Queues to store messages, and +- **Azure EventHubs**, which uses Azure EventHubs :::zone-end diff --git a/docs/orleans/streaming/stream-providers.md b/docs/orleans/streaming/stream-providers.md index 1be2554f05d32..2541f4f2f5ac0 100644 --- a/docs/orleans/streaming/stream-providers.md +++ b/docs/orleans/streaming/stream-providers.md @@ -1,31 +1,32 @@ --- title: Orleans stream providers description: Learn about the available stream providers for .NET Orleans. -ms.date: 07/03/2024 +ms.date: 05/23/2025 +ms.topic: conceptual zone_pivot_groups: orleans-version --- # Orleans stream providers -Streams can come in different shapes and forms. Some streams may deliver events over direct TCP links, while others deliver events via durable queues. Different stream types may use different batching strategies, different caching algorithms, or different backpressure procedures. Stream providers are extensibility points to Orleans Streaming Runtime that allow users to implement any type of stream, which avoids constraining streaming applications to only a subset of those behavioral choices. This extensibility point is similar in spirit to Orleans Storage providers. +Streams can come in different shapes and forms. Some streams might deliver events over direct TCP links, while others deliver events via durable queues. Different stream types might use different batching strategies, caching algorithms, or backpressure procedures. Stream providers are extensibility points in the Orleans Streaming Runtime that allow you to implement any type of stream, avoiding constraints on streaming applications to only a subset of those behavioral choices. This extensibility point is similar in spirit to Orleans storage providers. ## Azure Event Hub stream provider -[Azure Event Hub](/azure/event-hubs) is a fully managed, real-time data ingestion service that's capable of receiving and processing millions of events per second. It's designed to handle the high-throughput, low-latency ingestion of data from multiple sources and the subsequent processing of that data by multiple consumers. +[Azure Event Hubs](/azure/event-hubs) is a fully managed, real-time data ingestion service capable of receiving and processing millions of events per second. It's designed to handle high-throughput, low-latency data ingestion from multiple sources and subsequent processing of that data by multiple consumers. -Event Hubs is often used as the foundation of a larger event-processing architecture, where it serves as the "front door" for an event pipeline. It can be used to ingest data from a wide variety of sources, including social media feeds, IoT devices, and log files. One of the key benefits of Event Hubs is the ability to scale out horizontally to meet the needs of even the largest event-processing workloads. It's also highly available and fault-tolerant, with multiple replicas of data distributed across multiple Azure regions to ensure high availability. +Event Hubs is often used as the foundation of a larger event-processing architecture, serving as the "front door" for an event pipeline. You can use it to ingest data from various sources, including social media feeds, IoT devices, and log files. One of the key benefits of Event Hubs is the ability to scale out horizontally to meet the needs of even the largest event-processing workloads. It's also highly available and fault-tolerant, with multiple data replicas distributed across multiple Azure regions to ensure high availability. The [Microsoft.Orleans.Streaming.EventHubs](https://www.nuget.org/packages/Microsoft.Orleans.Streaming.EventHubs) NuGet package contains the Event Hubs stream provider. ## Azure Queue (AQ) stream provider -Azure Queue (AQ) stream provider delivers events over Azure Queues. On the producer side, AQ stream provider enqueues events directly into Azure Queue. On the consumer side, the AQ stream provider manages a set of **pulling agents** that pull events from a set of Azure Queues and deliver them to the application code that consumes them. One can think of the pulling agents as a distributed "micro-service"—a partitioned, highly available, and elastic distributed component. The pulling agents run inside the same silos that host application grains. Thus, there is no need to run separate Azure worker roles to pull from the queues. The existence of pulling agents, their management, backpressure, balancing the queues between them, and handing off queues from a failed agent to another agent are fully managed by Orleans Streaming Runtime and are transparent to application code that uses streams. +The Azure Queue (AQ) stream provider delivers events over Azure Queues. On the producer side, the AQ stream provider enqueues events directly into Azure Queue. On the consumer side, the AQ stream provider manages a set of **pulling agents** that pull events from a set of Azure Queues and deliver them to the application code that consumes them. You can think of the pulling agents as a distributed "microservice"—a partitioned, highly available, and elastic distributed component. The pulling agents run inside the same silos hosting application grains. Thus, there's no need to run separate Azure worker roles to pull from the queues. The Orleans Streaming Runtime fully manages the existence of pulling agents, their management, backpressure, balancing the queues between them, and handing off queues from a failed agent to another agent. This is all transparent to application code using streams. The [Microsoft.Orleans.Streaming.AzureStorage](https://www.nuget.org/packages/Microsoft.Orleans.Streaming.AzureStorage) NuGet package contains the [Azure Queue storage](https://azure.microsoft.com/products/storage/queues) stream provider. ## Queue adapters -Different stream providers that deliver events over durable queues exhibit similar behavior and are subject to a similar implementation. Therefore, we provide a generic extensible allows developers to plug in different types of queues without writing a completely new stream provider from scratch. `PersistentStreamProvider` uses an component, which abstracts specific queue implementation details and provides means to enqueue and dequeue events. All the rest is handled by the logic inside the `PersistentStreamProvider`. Azure Queue Provider mentioned above is also implemented this way: it is an instance of `PersistentStreamProvider` that uses an `AzureQueueAdapter`. +Different stream providers delivering events over durable queues exhibit similar behavior and are subject to similar implementations. Therefore, we provide a generic extensible that allows you to plug in different types of queues without writing a completely new stream provider from scratch. `PersistentStreamProvider` uses an component, which abstracts specific queue implementation details and provides means to enqueue and dequeue events. The logic inside `PersistentStreamProvider` handles everything else. The Azure Queue Provider mentioned above is also implemented this way: it's an instance of `PersistentStreamProvider` that uses an `AzureQueueAdapter`. :::zone target="docs" pivot="orleans-7-0" @@ -38,10 +39,10 @@ Different stream providers that deliver events over durable queues exhibit simil ## Simple message stream provider -The simple message stream provider, also known as the SMS provider, delivers events over TCP by utilizing regular Orleans grain messaging. Since events in SMS are delivered over unreliable TCP links, SMS does _not_ guarantee reliable event delivery and does not automatically resend failed messages for SMS streams. By default, the producer's call to returns a `Task` that represents the processing status of the stream consumer, which tells the producer whether the consumer successfully received and processed the event. If this task fails, the producer can decide to send the same event again, thus achieving reliability on the application level. Although stream message delivery is the best effort, SMS streams themselves are reliable. That is, the subscriber-to-producer binding performed by Pub-Sub is fully reliable. +The simple message stream provider, also known as the SMS provider, delivers events over TCP using regular Orleans grain messaging. Since SMS events are delivered over unreliable TCP links, SMS does _not_ guarantee reliable event delivery and doesn't automatically resend failed messages for SMS streams. By default, the producer's call to returns a `Task` representing the stream consumer's processing status. This tells the producer whether the consumer successfully received and processed the event. If this task fails, the producer can decide to send the same event again, achieving reliability at the application level. Although stream message delivery is best-effort, SMS streams themselves are reliable. That is, the subscriber-to-producer binding performed by Pub-Sub is fully reliable. :::zone-end ## See also -[Orleans Streams Implementation Details](../implementation/streams-implementation/index.md) +- [Orleans streams implementation details](../implementation/streams-implementation/index.md) diff --git a/docs/orleans/streaming/streams-programming-apis.md b/docs/orleans/streaming/streams-programming-apis.md index 35e0bda9e22fc..cfc6e0e71bf50 100644 --- a/docs/orleans/streaming/streams-programming-apis.md +++ b/docs/orleans/streaming/streams-programming-apis.md @@ -1,17 +1,18 @@ --- title: Orleans streaming APIs description: Learn about the available streaming APIs for .NET Orleans. -ms.date: 07/03/2024 +ms.date: 03/30/2025 +ms.topic: conceptual zone_pivot_groups: orleans-version --- # Orleans streaming APIs -Applications interact with streams via APIs that are very similar to the well-known [Reactive Extensions (Rx) in .NET](/previous-versions/dotnet/reactive-extensions/hh242985(v=vs.103)). The main difference is that Orleans stream extensions are **asynchronous**, to make processing more efficient in Orleans' distributed and scalable compute fabric. +Applications interact with streams via APIs very similar to the well-known [Reactive Extensions (Rx) in .NET](/previous-versions/dotnet/reactive-extensions/hh242985(v=vs.103)). The main difference is that Orleans stream extensions are **asynchronous** to make processing more efficient in Orleans' distributed and scalable compute fabric. ### Async stream -An application starts by using a [*stream provider*](stream-providers.md) to get a handle to a stream. You can think of a stream provider as a stream factory that allows implementers to customize streams behavior and semantics: +You start by using a [*stream provider*](stream-providers.md) to get a handle to a stream. You can think of a stream provider as a stream factory that allows implementers to customize streams behavior and semantics: :::zone target="docs" pivot="orleans-7-0" @@ -35,13 +36,13 @@ IAsyncStream stream = streamProvider.GetStream(Guid, "MyStreamNamespace"); :::zone-end -An application can get a reference to the stream provider either by calling the method when inside a grain, or by calling the method when on the client. +You can get a reference to the stream provider either by calling the method when inside a grain or by calling the `GetStreamProvider` method on the client instance. - is a logical, strongly typed handle to a virtual stream. It's similar in spirit to Orleans Grain Reference. Calls to `GetStreamProvider` and `GetStream` are purely local. The arguments to `GetStream` are a GUID and an additional string that we call a stream namespace (which can be null). Together the GUID and the namespace string comprise the stream identity (similar in spirit to the arguments to ). The combination of GUID and namespace string provide extra flexibility in determining stream identities. Just like grain 7 may exist within the Grain type `PlayerGrain` and a different grain 7 may exist within the grain type `ChatRoomGrain`, Stream 123 may exist with the stream namespace `PlayerEventsStream` and a different stream 123 may exist within the stream namespace `ChatRoomMessagesStream`. + is a logical, strongly typed handle to a virtual stream, similar in spirit to an Orleans Grain Reference. Calls to `GetStreamProvider` and `GetStream` are purely local. The arguments to `GetStream` are a GUID and an additional string called a stream namespace (which can be null). Together, the GUID and the namespace string comprise the stream identity (similar to the arguments for ). This combination provides extra flexibility in determining stream identities. Just like grain 7 might exist within the `PlayerGrain` type and a different grain 7 might exist within the `ChatRoomGrain` type, Stream 123 can exist within the `PlayerEventsStream` namespace, and a different stream 123 can exist within the `ChatRoomMessagesStream` namespace. ### Producing and consuming - implements both the and interfaces. That way an application can use the stream either to produce new events into the stream by using `Orleans.Streams.IAsyncObserver` or to subscribe to and consume events from a stream by using `Orleans.Streams.IAsyncObservable`. + implements both the and interfaces. This allows your application to use the stream either to produce new events using `IAsyncObserver` or to subscribe to and consume events using `IAsyncObservable`. ```csharp public interface IAsyncObserver @@ -57,31 +58,31 @@ public interface IAsyncObservable } ``` -To produce events into the stream, an application just calls +To produce events into the stream, your application calls: ```csharp await stream.OnNextAsync(event) ``` -To subscribe to a stream, an application calls +To subscribe to a stream, your application calls: ```csharp StreamSubscriptionHandle subscriptionHandle = await stream.SubscribeAsync(IAsyncObserver) ``` -The argument to can either be an object that implements the interface or a combination of lambda functions to process incoming events. More options for `SubscribeAsync` are available via class. `SubscribeAsync` returns a , which is an opaque handle that can be used to unsubscribe from the stream (similar in spirit to an asynchronous version of ). +The argument to can be either an object implementing the interface or a combination of lambda functions to process incoming events. More options for `SubscribeAsync` are available via the class. `SubscribeAsync` returns a , an opaque handle used to unsubscribe from the stream (similar to an asynchronous version of ). ```csharp await subscriptionHandle.UnsubscribeAsync() ``` -It is important to note that the subscription is for a grain, not for activation. Once the grain code is subscribed to the stream, this subscription surpasses the life of this activation and stays durable forever, until the grain code (potentially in a different activation) explicitly unsubscribes. This is the heart of a virtual stream abstraction: not only do all streams always exist, logically, but also a stream subscription is durable and lives beyond a particular physical activation that created the subscription. +It's important to note that the subscription is for a grain, not for activation. Once the grain code subscribes to the stream, this subscription surpasses the life of this activation and remains durable forever until the grain code (potentially in a different activation) explicitly unsubscribes. This is the core of the virtual stream abstraction: not only do all streams always exist logically, but a stream subscription is also durable and lives beyond the particular physical activation that created it. ### Multiplicity -An Orleans stream may have multiple producers and multiple consumers. A message published by a producer will be delivered to all consumers that were subscribed to the stream before the message was published. +An Orleans stream can have multiple producers and multiple consumers. A message published by a producer is delivered to all consumers subscribed to the stream before the message was published. -In addition, the consumer can subscribe to the same stream multiple times. Each time it subscribes it gets back a unique . If a grain (or client) is subscribed X times to the same stream, it will receive the same event X times, once for each subscription. The consumer can also unsubscribe from an individual subscription. It can find all its current subscriptions by calling: +Additionally, a consumer can subscribe to the same stream multiple times. Each time it subscribes, it gets back a unique . If a grain (or client) subscribes X times to the same stream, it receives the same event X times, once for each subscription. The consumer can also unsubscribe from an individual subscription. You can find all current subscriptions by calling: ```csharp IList> allMyHandles = @@ -90,38 +91,38 @@ IList> allMyHandles = ### Recovering from failures -If the producer of a stream dies (or its grain is deactivated), there is nothing it needs to do. The next time this grain wants to produce more events it can get the stream handle again and produce new events in the same way. +If the producer of a stream dies (or its grain is deactivated), it doesn't need to do anything. The next time this grain wants to produce more events, it can get the stream handle again and produce new events as usual. -Consumer logic is a little bit more involved. As we said before, once a consumer grain is subscribed to a stream, this subscription is valid until the grain explicitly unsubscribes. If the consumer of the stream dies (or its grain is deactivated) and a new event is generated on the stream, the consumer grain will be automatically re-activated (just like any regular Orleans grain is automatically activated when a message is sent to it). The only thing that the grain code needs to do now is to provide an to process the data. The consumer needs to re-attach processing logic as part of the method. To do that it can call: +Consumer logic is slightly more involved. As mentioned before, once a consumer grain subscribes to a stream, this subscription is valid until the grain explicitly unsubscribes. If the consumer of the stream dies (or its grain is deactivated) and a new event is generated on the stream, the consumer grain automatically reactivates (just like any regular Orleans grain automatically activates when a message is sent to it). The only thing the grain code needs to do now is provide an to process the data. The consumer needs to re-attach processing logic as part of the method. To do that, it can call: ```csharp StreamSubscriptionHandle newHandle = await subscriptionHandle.ResumeAsync(IAsyncObserver); ``` -The consumer uses the previous handle it got when it first subscribed to "resume processing". Notice that merely updates an existing subscription with the new instance of `IAsyncObserver` logic and does not change the fact that this consumer is already subscribed to this stream. +The consumer uses the previous handle obtained during the initial subscription to "resume processing". Note that merely updates an existing subscription with the new instance of `IAsyncObserver` logic and doesn't change the fact that this consumer is already subscribed to this stream. -How does the consumer get an old `subscriptionHandle`? There are 2 options. The consumer may have persisted the handle it was given back from the original `SubscribeAsync` operation and can use it now. Alternatively, if the consumer does not have the handle, it can ask the `IAsyncStream` for all its active subscription handles, by calling: +How does the consumer get the old `subscriptionHandle`? There are two options. The consumer might have persisted the handle returned from the original `SubscribeAsync` operation and can use it now. Alternatively, if the consumer doesn't have the handle, it can ask the `IAsyncStream` for all its active subscription handles by calling: ```csharp IList> allMyHandles = await IAsyncStream.GetAllSubscriptionHandles(); ``` -The consumer can now resume all of them or unsubscribe from some if it wishes to. +The consumer can then resume all of them or unsubscribe from some if desired. > [!TIP] -> If the consumer grain implements the interface directly (`public class MyGrain : Grain, IAsyncObserver`), it should in theory not be required to re-attach the `IAsyncObserver` and thus will not need to call `ResumeAsync`. The streaming runtime should be able to automatically figure out that the grain already implements `IAsyncObserver` and will just invoke those `IAsyncObserver` methods. However, the streaming runtime currently does not support this and the grain code still needs to explicitly call `ResumeAsync`, even if the grain implements `IAsyncObserver` directly. +> If the consumer grain implements the interface directly (`public class MyGrain : Grain, IAsyncObserver`), it shouldn't theoretically need to re-attach the `IAsyncObserver` and thus wouldn't need to call `ResumeAsync`. The streaming runtime should automatically figure out that the grain already implements `IAsyncObserver` and invoke those `IAsyncObserver` methods. However, the streaming runtime currently doesn't support this, and the grain code still needs to explicitly call `ResumeAsync`, even if the grain implements `IAsyncObserver` directly. ### Explicit and implicit subscriptions -By default, a stream consumer has to explicitly subscribe to the stream. This subscription would usually be triggered by some external message that the grain (or client) receives that instructs it to subscribe. For example, in a chat service when a user joins a chat room his grain receives a `JoinChatGroup` message with the chat name, which will cause the user grain to subscribe to this chat stream. +By default, a stream consumer must explicitly subscribe to the stream. This subscription is usually triggered by an external message the grain (or client) receives instructing it to subscribe. For example, in a chat service, when a user joins a chat room, their grain receives a `JoinChatGroup` message with the chat name, causing the user grain to subscribe to this chat stream. -In addition, Orleans streams also support *implicit subscriptions*. In this model, the grain does not explicitly subscribe to the stream. This grain is subscribed automatically, implicitly, just based on its grain identity and an . Implicit subscriptions' main value is allowing the stream activity to trigger the grain activation (hence triggering the subscription) automatically. For example, using SMS streams, if one grain wanted to produce a stream and another grain process this stream, the producer would need to know the identity of the consumer grain and make a grain call to it telling it to subscribe to the stream. Only after that can it start sending events. Instead, using implicit subscriptions, the producer can just start producing events to a stream, and the consumer grain will automatically be activated and subscribe to the stream. In that case, the producer doesn't care at all who is reading the events +Additionally, Orleans streams support *implicit subscriptions*. In this model, the grain doesn't explicitly subscribe. It's subscribed automatically and implicitly based on its grain identity and an . The main value of implicit subscriptions is allowing stream activity to trigger grain activation (and thus the subscription) automatically. For example, using SMS streams, if one grain wanted to produce a stream and another grain process it, the producer would need the consumer grain's identity and make a grain call telling it to subscribe. Only then could it start sending events. Instead, with implicit subscriptions, the producer can just start producing events to a stream, and the consumer grain automatically activates and subscribes. In this case, the producer doesn't need to know who is reading the events. -The grain implementation `MyGrainType` can declare an attribute `[ImplicitStreamSubscription("MyStreamNamespace")]`. This tells the streaming runtime that when an event is generated on a stream whose identity is GUID XXX and `"MyStreamNamespace"` namespace, it should be delivered to the grain whose identity is XXX of type `MyGrainType`. That is, the runtime maps stream `` to consumer grain ``. +The grain implementation `MyGrainType` can declare an attribute `[ImplicitStreamSubscription("MyStreamNamespace")]`. This tells the streaming runtime that when an event is generated on a stream with identity GUID XXX and namespace `"MyStreamNamespace"`, it should be delivered to the grain with identity XXX of type `MyGrainType`. That is, the runtime maps stream `` to consumer grain ``. -The presence of `ImplicitStreamSubscription`causes the streaming runtime to automatically subscribe this grain to a stream and deliver the stream events to it. However, the grain code still needs to tell the runtime how it wants events to be processed. Essentially, it needs to attach the `IAsyncObserver`. Therefore, when the grain is activated, the grain code inside `OnActivateAsync` needs to call: +The presence of `ImplicitStreamSubscription` causes the streaming runtime to automatically subscribe this grain to the stream and deliver stream events to it. However, the grain code still needs to tell the runtime how it wants events processed. Essentially, it needs to attach the `IAsyncObserver`. Therefore, when the grain activates, the grain code inside `OnActivateAsync` needs to call: :::zone target="docs" pivot="orleans-7-0" @@ -160,13 +161,13 @@ StreamSubscriptionHandle subscription = ### Writing subscription logic -Below are the guidelines on how to write the subscription logic for various cases: explicit and implicit subscriptions, rewindable and non-rewindable streams. The main difference between explicit and implicit subscriptions is that for implicit the grain always has exactly one implicit subscription for every stream namespace; there is no way to create multiple subscriptions (there is no subscription multiplicity), there is no way to unsubscribe, and the grain logic always only needs to attach the processing logic. That also means that for implicit subscriptions there is never a need to resume a subscription. On the other hand, for explicit subscriptions, one needs to Resume the subscription, otherwise, if the grain subscribes again it will result in the grain being subscribed multiple times. +Below are guidelines for writing subscription logic for various cases: explicit and implicit subscriptions, rewindable and non-rewindable streams. The main difference between explicit and implicit subscriptions is that for implicit subscriptions, the grain always has exactly one implicit subscription per stream namespace. There's no way to create multiple subscriptions (no subscription multiplicity), no way to unsubscribe, and the grain logic only ever needs to attach the processing logic. This also means there's never a need to resume an implicit subscription. On the other hand, for explicit subscriptions, you need to resume the subscription; otherwise, subscribing again results in the grain being subscribed multiple times. **Implicit subscriptions:** -For implicit subscriptions, the grain still needs to subscribe to attach the processing logic. This can be done in the consumer grain by implementing the `IStreamSubscriptionObserver` and `IAsyncObserver` interfaces, allowing the grain to activate separately from subscribing. To subscribe to the stream, the grain creates a handle and calls `await handle.ResumeAsync(this)` in its `OnSubscribed(...)` method. +For implicit subscriptions, the grain still needs to subscribe to attach the processing logic. You can do this in the consumer grain by implementing the `IStreamSubscriptionObserver` and `IAsyncObserver` interfaces, allowing the grain to activate separately from subscribing. To subscribe to the stream, the grain creates a handle and calls `await handle.ResumeAsync(this)` in its `OnSubscribed(...)` method. -To process messages, the `IAsyncObserver.OnNextAsync(...)` method is implemented to receive stream data and a sequence token. Alternatively, the `ResumeAsync` method may take a set of delegates representing the methods of the `IAsyncObserver` interface, `onNextAsync`, `onErrorAsync`, and `onCompletedAsync`. +To process messages, implement the `IAsyncObserver.OnNextAsync(...)` method to receive stream data and a sequence token. Alternatively, the `ResumeAsync` method can take a set of delegates representing the methods of the `IAsyncObserver` interface: `onNextAsync`, `onErrorAsync`, and `onCompletedAsync`. :::zone target="docs" pivot="orleans-7-0" @@ -207,7 +208,7 @@ public override async Task OnActivateAsync() **Explicit subscriptions:** -For explicit subscriptions, a grain must call `SubscribeAsync` to subscribe to the stream. This creates a subscription, as well as attaches the processing logic. The explicit subscription will exist until the grain unsubscribes, so if a grain gets deactivated and reactivated, the grain is still explicitly subscribed, but no processing logic will be attached. In this case, the grain needs to re-attach the processing logic. To do that, in its `OnActivateAsync`, the grain first needs to find out what subscriptions it has, by calling . The grain must execute `ResumeAsync` on each handle it wishes to continue processing or UnsubscribeAsync on any handles it is done with. The grain can also optionally specify the `StreamSequenceToken` as an argument to the `ResumeAsync` calls, which will cause this explicit subscription to start consuming from that token. +For explicit subscriptions, a grain must call `SubscribeAsync` to subscribe to the stream. This creates a subscription and attaches the processing logic. The explicit subscription exists until the grain unsubscribes. If a grain deactivates and reactivates, it's still explicitly subscribed, but no processing logic is attached. In this case, the grain needs to re-attach the processing logic. To do this, in its `OnActivateAsync`, the grain first needs to find out its subscriptions by calling . The grain must execute `ResumeAsync` on each handle it wishes to continue processing or `UnsubscribeAsync` on any handles it's done with. The grain can also optionally specify the `StreamSequenceToken` as an argument to the `ResumeAsync` calls, causing this explicit subscription to start consuming from that token. :::zone target="docs" pivot="orleans-7-0" @@ -253,44 +254,44 @@ public async override Task OnActivateAsync() ### Stream order and sequence tokens -The order of event delivery between an individual producer and an individual consumer depends on the stream provider. +The order of event delivery between an individual producer and consumer depends on the stream provider. -With SMS the producer explicitly controls the order of events seen by the consumer by controlling the way the producer publishes them. By default (if the option for SMS provider is set to false) and if the producer awaits every `OnNextAsync` call, the events arrive in FIFO order. In SMS it is up to the producer to decide how to handle delivery failures that will be indicated by a broken `Task` returned by the `OnNextAsync` call. +With SMS, the producer explicitly controls the order of events seen by the consumer by controlling how they publish them. By default (if the option for the SMS provider is `false`) and if the producer awaits every `OnNextAsync` call, events arrive in FIFO order. In SMS, the producer decides how to handle delivery failures indicated by a broken `Task` returned by the `OnNextAsync` call. -Azure Queue streams do not guarantee FIFO order, since the underlying Azure Queues do not guarantee the order in failure cases. (They do guarantee FIFO order in failure-free executions.) When a producer produces the event into Azure Queue, if the queue operation fails, it is up to the producer to attempt another queue and later on deal with potential duplicate messages. On the delivery side, the Orleans Streaming runtime dequeues the event from the queue and attempts to deliver it for processing to consumers. The Orleans Streaming runtime deletes the event from the queue only upon successful processing. If the delivery or processing fails, the event is not deleted from the queue and will automatically re-appear in the queue later. The Streaming runtime will try to deliver it again, thus potentially breaking the FIFO order. The above behavior matches the normal semantics of Azure Queues. +Azure Queue streams don't guarantee FIFO order because the underlying Azure Queues don't guarantee order in failure cases (though they do guarantee FIFO order in failure-free executions). When a producer produces an event into an Azure Queue, if the queue operation fails, the producer must attempt another queue and later deal with potential duplicate messages. On the delivery side, the Orleans Streaming runtime dequeues the event and attempts to deliver it for processing to consumers. The runtime deletes the event from the queue only upon successful processing. If delivery or processing fails, the event isn't deleted from the queue and automatically reappears later. The Streaming runtime tries to deliver it again, potentially breaking FIFO order. This behavior matches the normal semantics of Azure Queues. -**Application Defined Order**: To deal with the above ordering issues, an application can optionally specify its ordering. This is achieved via a , which is an opaque object that can be used to order events. A producer can pass an optional `StreamSequenceToken` to the `OnNext` call. This `StreamSequenceToken` will be passed to the consumer and will be delivered together with the event. That way, an application can reason and reconstruct its order independently of the streaming runtime. +**Application-defined order**: To handle the ordering issues above, your application can optionally specify its ordering. Achieve this using a , an opaque object used to order events. A producer can pass an optional `StreamSequenceToken` to the `OnNextAsync` call. This `StreamSequenceToken` passes to the consumer and is delivered with the event. This way, your application can reason about and reconstruct its order independently of the streaming runtime. ### Rewindable streams -Some streams only allow an application to subscribe to them starting at the latest point in time, while other streams allow "going back in time". The latter capability is dependent on the underlying queuing technology and the particular stream provider. For example, Azure Queues only allow consuming the latest enqueued events, while EventHub allows replaying events from an arbitrary point in time (up to some expiration time). Streams that support going back in time are called *rewindable streams*. +Some streams only allow subscribing starting from the latest point in time, while others allow "going back in time." This capability depends on the underlying queuing technology and the specific stream provider. For example, Azure Queues only allow consuming the latest enqueued events, while Event Hubs allows replaying events from an arbitrary point in time (up to some expiration time). Streams supporting going back in time are called *rewindable streams*. -The consumer of a rewindable stream can pass a `StreamSequenceToken` to the `SubscribeAsync` call. The runtime will deliver events to it starting from that `StreamSequenceToken`. A null token means the consumer wants to receive events starting from the latest. +The consumer of a rewindable stream can pass a `StreamSequenceToken` to the `SubscribeAsync` call. The runtime delivers events starting from that `StreamSequenceToken`. A null token means the consumer wants to receive events starting from the latest. -The ability to rewind a stream is very useful in recovery scenarios. For example, consider a grain that subscribes to a stream and periodically checkpoints its state together with the latest sequence token. When recovering from a failure, the grain can re-subscribe to the same stream from the latest checkpointed sequence token, thereby recovering without losing any events that were generated since the last checkpoint. +The ability to rewind a stream is very useful in recovery scenarios. For example, consider a grain that subscribes to a stream and periodically checkpoints its state along with the latest sequence token. When recovering from a failure, the grain can re-subscribe to the same stream from the latest checkpointed sequence token, recovering without losing any events generated since the last checkpoint. -[Event Hubs provider](https://www.nuget.org/packages/Microsoft.Orleans.OrleansServiceBus/) is rewindable. You can find its code on [GitHub: Orleans/Azure/Orleans.Streaming.EventHubs](https://github.com/dotnet/orleans/tree/main/src/Azure/Orleans.Streaming.EventHubs). [SMS](https://www.nuget.org/packages/Microsoft.Orleans.OrleansProviders/) and [Azure Queue](https://www.nuget.org/packages/Microsoft.Orleans.Streaming.AzureStorage/) providers are _not_ rewindable. +The [Event Hubs provider](https://www.nuget.org/packages/Microsoft.Orleans.Streaming.EventHubs) is rewindable. You can find its code on [GitHub: Orleans/Azure/Orleans.Streaming.EventHubs](https://github.com/dotnet/orleans/tree/main/src/Azure/Orleans.Streaming.EventHubs). The SMS (now [Broadcast Channel](broadcast-channel.md)) and [Azure Queue](https://www.nuget.org/packages/Microsoft.Orleans.Streaming.AzureStorage) providers are _not_ rewindable. ### Stateless automatically scaled-out processing -By default, Orleans Streaming is targeted to support a large number of relatively small streams, each processed by one or more stateful grains. Collectively, the processing of all the streams together is sharded among a large number of regular (stateful) grains. The application code controls this sharding by assigning stream ids and grain ids and by explicitly subscribing. The goal is sharded stateful processing. +By default, Orleans Streaming targets supporting a large number of relatively small streams, each processed by one or more stateful grains. Collectively, the processing of all streams is sharded among many regular (stateful) grains. Your application code controls this sharding by assigning stream IDs and grain IDs and by explicitly subscribing. The goal is sharded stateful processing. -However, there is also an interesting scenario of automatically scaled-out stateless processing. In this scenario, an application has a small number of streams (or even one large stream) and the goal is stateless processing. An example is a global stream of events, where the processing involves decoding each event and potentially forwarding it to other streams for further stateful processing. The stateless scaled-out stream processing can be supported in Orleans via grains. +However, there's also an interesting scenario of automatically scaled-out stateless processing. In this scenario, an application has a small number of streams (or even one large stream), and the goal is stateless processing. An example is a global stream of events where processing involves decoding each event and potentially forwarding it to other streams for further stateful processing. Stateless scaled-out stream processing can be supported in Orleans via grains. -**Current Status of Stateless Automatically Scaled-Out Processing:** -This is not yet implemented. An attempt to subscribe to a stream from a `StatelessWorker` grain will result in undefined behavior. [We are considering to support this option](https://github.com/dotnet/orleans/issues/433). +**Current status of stateless automatically scaled-out processing:** +This isn't yet implemented. Attempting to subscribe to a stream from a `StatelessWorker` grain results in undefined behavior. [We are considering supporting this option](https://github.com/dotnet/orleans/issues/433). ### Grains and Orleans clients -Orleans streams work uniformly across grains and Orleans clients. That is, the same APIs can be used inside a grain and in an Orleans client to produce and consume events. This greatly simplifies the application logic, making special client-side APIs, such as Grain Observers, redundant. +Orleans streams work uniformly across grains and Orleans clients. That means you can use the same APIs inside a grain and in an Orleans client to produce and consume events. This greatly simplifies application logic, making special client-side APIs like Grain Observers redundant. ### Fully managed and reliable streaming pub-sub -To track stream subscriptions, Orleans uses a runtime component called **Streaming Pub-Sub** which serves as a rendezvous point for stream consumers and stream producers. Pub-sub tracks all stream subscriptions and persists them, and matches stream consumers with stream producers. +To track stream subscriptions, Orleans uses a runtime component called **Streaming Pub-Sub**, which serves as a rendezvous point for stream consumers and producers. Pub-sub tracks all stream subscriptions, persists them, and matches stream consumers with stream producers. -Applications can choose where and how the Pub-Sub data is stored. The Pub-Sub component itself is implemented as grains (called `PubSubRendezvousGrain`), which use Orleans declarative persistence. `PubSubRendezvousGrain` uses the storage provider named `PubSubStore`. As with any grain, you can designate an implementation for a storage provider. For Streaming Pub-Sub you can change the implementation of the `PubSubStore` at silo construction time using the silo host builder: +Applications can choose where and how the Pub-Sub data is stored. The Pub-Sub component itself is implemented as grains (called `PubSubRendezvousGrain`), which use Orleans declarative persistence. `PubSubRendezvousGrain` uses the storage provider named `PubSubStore`. As with any grain, you can designate an implementation for a storage provider. For Streaming Pub-Sub, you can change the implementation of the `PubSubStore` at silo construction time using the silo host builder: -The following configures the Pub-Sub to store its state in Azure tables. +The following configures Pub-Sub to store its state in Azure tables. :::zone target="docs" pivot="orleans-7-0" @@ -314,7 +315,7 @@ hostBuilder.AddAzureTableGrainStorage("PubSubStore", :::zone-end -That way Pub-Sub data will be durably stored in Azure Table. For initial development, you can use memory storage as well. In addition to Pub-Sub, the Orleans Streaming Runtime delivers events from producers to consumers, manages all runtime resources allocated to actively used streams, and transparently garbage collects runtime resources from unused streams. +This way, Pub-Sub data is durably stored in Azure Table. For initial development, you can use memory storage as well. In addition to Pub-Sub, the Orleans Streaming Runtime delivers events from producers to consumers, manages all runtime resources allocated to actively used streams, and transparently garbage collects runtime resources from unused streams. ### Configuration diff --git a/docs/orleans/streaming/streams-quick-start.md b/docs/orleans/streaming/streams-quick-start.md index d15bc30f1de06..dfd736a622305 100644 --- a/docs/orleans/streaming/streams-quick-start.md +++ b/docs/orleans/streaming/streams-quick-start.md @@ -1,13 +1,14 @@ --- title: Orleans streaming quickstart description: Learn from the streaming quickstart in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 03/30/2025 +ms.topic: quickstart zone_pivot_groups: orleans-version --- # Orleans streaming quickstart -This guide will show you a quick way to set up and use Orleans Streams. To learn more about the details of the streaming features, read other parts of this documentation. +This guide shows you a quick way to set up and use Orleans Streams. To learn more about the details of streaming features, read other parts of this documentation. ## Required configurations @@ -15,7 +16,7 @@ This guide will show you a quick way to set up and use Orleans Streams. To learn :::zone target="docs" pivot="orleans-7-0" -In this guide, you'll use a memory-based stream that uses grain messaging to send stream data to subscribers. You will use the in-memory storage provider to store lists of subscriptions. Using memory-based mechanisms for streaming and storage is only intended for local development and testing, and isn't intended for production environments. +In this guide, you use a memory-based stream that uses grain messaging to send stream data to subscribers. You use the in-memory storage provider to store lists of subscriptions. Using memory-based mechanisms for streaming and storage is intended only for local development and testing, not for production environments. On the silo, where `silo` is an , call : @@ -35,7 +36,7 @@ client.AddMemoryStreams("StreamProvider"); :::zone target="docs" pivot="orleans-3-x" -In this guide, we'll use a simple message-based stream that uses grain messaging to send stream data to subscribers. We will use the in-memory storage provider to store lists of subscriptions, so it is not a wise choice for real production applications. +In this guide, use a simple message-based stream that uses grain messaging to send stream data to subscribers. Use the in-memory storage provider to store lists of subscriptions; this isn't a wise choice for real production applications. On the silo, where `hostBuilder` is an `ISiloHostBuilder`, call : @@ -51,7 +52,7 @@ clientBuilder.AddSimpleMessageStreamProvider("SMSProvider"); ``` > [!NOTE] -> By default, messages that are passed over the Simple Message Stream are considered immutable, and may be passed by reference to other grains. To turn off this behavior, you must config the SMS provider to turn off +> By default, messages passed over the Simple Message Stream are considered immutable and might be passed by reference to other grains. To turn off this behavior, configure the SMS provider to turn off . ```csharp siloBuilder @@ -62,7 +63,7 @@ siloBuilder :::zone-end -You can create streams, send data using them as producers and also receive data as subscribers. +You can create streams, send data using them as producers, and receive data as subscribers. ## Produce events @@ -70,7 +71,7 @@ You can create streams, send data using them as producers and also receive data :::zone target="docs" pivot="orleans-7-0" -It's relatively easy to produce events for streams. You should first get access to the stream provider that you defined in the config previously (`"StreamProvider"`), and then choose a stream and push data to it. +It's relatively easy to produce events for streams. First, get access to the stream provider defined in the config previously (`"StreamProvider"`), then choose a stream and push data to it. ```csharp // Pick a GUID for a chat room grain and chat room stream @@ -88,7 +89,7 @@ var stream = streamProvider.GetStream(streamId); :::zone target="docs" pivot="orleans-3-x" -It's relatively easy to produce events for streams. You should first get access to the stream provider that you defined in the config previously (`"SMSProvider"`), and then choose a stream and push data to it. +It's relatively easy to produce events for streams. First, get access to the stream provider defined in the config previously (`"SMSProvider"`), then choose a stream and push data to it. ```csharp // Pick a GUID for a chat room grain and chat room stream @@ -101,9 +102,9 @@ var stream = streamProvider.GetStream(guid, "RANDOMDATA"); :::zone-end -As you can see, our stream has a GUID and a namespace. This will make it easy to identify unique streams. For example, the namespace for a chat room can be "Rooms" and the GUID can be the owning RoomGrain's GUID. +As you can see, the stream has a GUID and a namespace. This makes it easy to identify unique streams. For example, the namespace for a chat room could be "Rooms", and the GUID could be the owning `RoomGrain`'s GUID. -Here we use the GUID of some known chat room. Using the `OnNextAsync` method of the stream we can push data to it. Let's do it inside a timer, using random numbers. You could use any other data type for the stream as well. +Here, use the GUID of a known chat room. Using the `OnNextAsync` method of the stream, push data to it. Let's do this inside a timer using random numbers. You could use any other data type for the stream as well. ```csharp RegisterTimer(_ => @@ -117,7 +118,7 @@ TimeSpan.FromMilliseconds(1_000)); ## Subscribe to and receive streaming data -For receiving data, you can use implicit and explicit subscriptions, which are described in more detail at [Explicit and implicit subscriptions](streams-programming-apis.md#explicit-and-implicit-subscriptions). This example uses implicit subscriptions, which are easier. When a grain type wants to implicitly subscribe to a stream, it uses the attribute [[ImplicitStreamSubscription(namespace)]](xref:Orleans.ImplicitStreamSubscriptionAttribute). +For receiving data, you can use implicit and explicit subscriptions, described in more detail at [Explicit and implicit subscriptions](streams-programming-apis.md#explicit-and-implicit-subscriptions). This example uses implicit subscriptions, which are easier. When a grain type wants to implicitly subscribe to a stream, it uses the attribute `[ImplicitStreamSubscription(namespace)]`. For your case, define a `ReceiverGrain` like this: @@ -126,9 +127,9 @@ For your case, define a `ReceiverGrain` like this: public class ReceiverGrain : Grain, IRandomReceiver ``` -Whenever data is pushed to the streams of the namespace `RANDOMDATA`, as we have in the timer, a grain of type `ReceiverGrain` with the same `Guid` of the stream will receive the message. Even if no activations of the grain currently exist, the runtime will automatically create a new one and send the message to it. +Whenever data is pushed to streams in the `RANDOMDATA` namespace (as in the timer example), a grain of type `ReceiverGrain` with the same `Guid` as the stream receives the message. Even if no activations of the grain currently exist, the runtime automatically creates a new one and sends the message to it. -For this to work, we need to complete the subscription process by setting our `OnNextAsync` method for receiving data. To do so, our `ReceiverGrain` should call something like this in its `OnActivateAsync` +For this to work, complete the subscription process by setting the `OnNextAsync` method for receiving data. To do so, the `ReceiverGrain` should call something like this in its `OnActivateAsync`: :::zone target="docs" pivot="orleans-7-0" @@ -185,11 +186,11 @@ await stream.SubscribeAsync( :::zone-end -You're all set! Now the only requirement is that something triggers the producer grain's creation, and then it will register the timer and start sending random ints to all interested parties. +You're all set! Now, the only requirement is that something triggers the producer grain's creation. Then, it registers the timer and starts sending random integers to all interested parties. -Again, this guide skips lots of details and is only good for showing the big picture. Read other parts of this manual and other resources on RX to gain a good understanding of what is available and how. +Again, this guide skips many details and only provides a high-level overview. Read other parts of this manual and other resources on Rx to gain a good understanding of what's available and how it works. -Reactive programming can be a very powerful approach to solving many problems. You could for example use LINQ in the subscriber to filter numbers and do all sorts of interesting stuff. +Reactive programming can be a powerful approach to solving many problems. For example, you could use LINQ in the subscriber to filter numbers and perform various interesting operations. ## See also diff --git a/docs/orleans/streaming/streams-why.md b/docs/orleans/streaming/streams-why.md index fe8ec7a29a6fc..32aa6074ffa02 100644 --- a/docs/orleans/streaming/streams-why.md +++ b/docs/orleans/streaming/streams-why.md @@ -1,67 +1,68 @@ --- title: Why streams in Orleans? description: Learn why you'd want to use streams in .NET Orleans. -ms.date: 07/03/2024 +ms.date: 03/30/2025 +ms.topic: conceptual --- # Why streams in Orleans? -There are already a wide range of technologies that allow you to build stream processing systems. Those include systems to **durably store stream data** (for example, [Event Hubs](https://azure.microsoft.com/services/event-hubs/) and [Kafka](https://kafka.apache.org/)) and systems to express **compute operations** over stream data (for example, [Azure Stream Analytics](https://azure.microsoft.com/services/stream-analytics/), [Apache Storm](https://storm.apache.org/), and [Apache Spark Streaming](https://spark.apache.org/streaming/)). Those are great systems that allow you to build efficient data stream processing pipelines. +A wide range of technologies already exists for building stream processing systems. These include systems to **durably store stream data** (for example, [Event Hubs](https://azure.microsoft.com/services/event-hubs/) and [Kafka](https://kafka.apache.org/)) and systems to express **compute operations** over stream data (for example, [Azure Stream Analytics](https://azure.microsoft.com/services/stream-analytics/), [Apache Storm](https://storm.apache.org/), and [Apache Spark Streaming](https://spark.apache.org/streaming/)). These are great systems that allow you to build efficient data stream processing pipelines. ### Limitations of existing systems -However, those systems are not suitable for **fine-grained free-form compute over stream data**. The streaming compute systems mentioned above all allow you to specify a **unified data-flow graph of operations that are applied in the same way to all stream items**. This is a powerful model when data is uniform and you want to express the same set of transformation, filtering, or aggregation operations over this data. But there are other use cases where you need to express fundamentally different operations over different data items. And in some of them as part of this processing, you occasionally need to make an external call, such as invoke some arbitrary REST API. The unified data-flow stream processing engines either do not support those scenarios, support them in a limited and constrained way, or are inefficient in supporting them. This is because they are inherently optimized for a **large volume of similar items, and usually limited in terms of expressiveness, processing**. Orleans Streams target those other scenarios. +However, these systems aren't suitable for **fine-grained free-form compute over stream data**. The streaming compute systems mentioned above all allow you to specify a **unified data-flow graph of operations applied in the same way to all stream items**. This is a powerful model when data is uniform and you want to express the same set of transformation, filtering, or aggregation operations over this data. But other use cases require expressing fundamentally different operations over different data items. In some of these cases, as part of the processing, you might occasionally need to make an external call, such as invoking an arbitrary REST API. Unified data-flow stream processing engines either don't support these scenarios, support them in a limited and constrained way, or are inefficient in supporting them. This is because they are inherently optimized for a **large volume of similar items and are usually limited in terms of expressiveness and processing**. Orleans Streams target these other scenarios. ### Motivation -It all started with requests from Orleans users to support returning a sequence of items from a grain method call. As you can imagine, that was only the tip of the iceberg. They needed much more than that. +It all started with requests from Orleans users to support returning a sequence of items from a grain method call. As you can imagine, that was only the tip of the iceberg; they needed much more. -A typical scenario for Orleans Streams is when you have per-user streams and you want to perform **different processing for each user**, within the context of an individual user. We may have millions of users but some of them are interested in weather and can subscribe to weather alerts for a particular location, while some are interested in sports events; somebody else is tracking the status of a particular flight. Processing those events requires different logic, but you don't want to run two independent instances of stream processing. Some users are interested in only a particular stock and only if a certain external condition applies, a condition that may not necessarily be part of the stream data (and thus needs to be checked dynamically at runtime as part of processing). +A typical scenario for Orleans Streams is when you have per-user streams and want to perform **different processing for each user** within the context of that individual user. You might have millions of users, but some are interested in weather and subscribe to weather alerts for a particular location, while others are interested in sports events; someone else might be tracking the status of a particular flight. Processing these events requires different logic, but you don't want to run two independent instances of stream processing. Some users might be interested only in a particular stock and only if a certain external condition applies—a condition that might not necessarily be part of the stream data (and thus needs checking dynamically at runtime as part of processing). -Users change their interests all the time, hence their subscriptions to specific streams of events come and go dynamically, thus **the streaming topology changes dynamically and rapidly**. On top of that, **the processing logic per user evolves and changes dynamically as well, based on user state and external events**. External events may modify the processing logic for a particular user. For example, in a game cheating detection system, when a new way to cheat is discovered the processing logic needs to be updated with the new rule to detect this new violation. This needs to be done of course **without disrupting the ongoing processing pipeline**. Bulk data-flow stream processing engines were not built to support such scenarios. +Users change their interests all the time, so their subscriptions to specific event streams come and go dynamically. Thus, **the streaming topology changes dynamically and rapidly**. On top of that, **the processing logic per user evolves and changes dynamically based on user state and external events**. External events might modify the processing logic for a particular user. For example, in a game cheating detection system, when a new cheating method is discovered, the processing logic needs updating with the new rule to detect this violation. This must be done, of course, **without disrupting the ongoing processing pipeline**. Bulk data-flow stream processing engines weren't built to support such scenarios. -It goes almost without saying that such a system has to run on several network-connected machines, not on a single node. Hence, the processing logic has to be distributed in a scalable and elastic manner across a cluster of servers. +It goes almost without saying that such a system must run on several network-connected machines, not just a single node. Hence, the processing logic must be distributed scalably and elastically across a cluster of servers. -### New Requirements +### New requirements -We identified 4 basic requirements for our Stream Processing system that will allow it to target the above scenarios. +Four basic requirements were identified for a Stream Processing system to target the scenarios above: 1. Flexible stream processing logic -2. Support for highly dynamic topologies -3. Fine-grained stream granularity -4. Distribution +1. Support for highly dynamic topologies +1. Fine-grained stream granularity +1. Distribution #### Flexible stream processing logic -We want the system to support different ways of expressing the stream processing logic. The existing systems we mentioned above require the developer to write a declarative data-flow computation graph, usually by following a functional programming style. This limits the expressiveness and flexibility of the processing logic. Orleans streams are indifferent to the way processing logic is expressed. It can be expressed as a data-flow (for example, by using [Reactive Extensions (Rx) in .NET](/previous-versions/dotnet/reactive-extensions/hh242985(v=vs.103))); as a functional program; as a declarative query; or in a general imperative logic. The logic can be stateful or stateless, may or may not have side effects, and can trigger external actions. All power goes to the developer. +The system should support different ways of expressing stream processing logic. Existing systems mentioned above require developers to write a declarative data-flow computation graph, usually following a functional programming style. This limits the expressiveness and flexibility of the processing logic. Orleans streams are indifferent to how processing logic is expressed. It can be expressed as a data flow (e.g., using [Reactive Extensions (Rx) in .NET](/previous-versions/dotnet/reactive-extensions/hh242985(v=vs.103))), a functional program, a declarative query, or general imperative logic. The logic can be stateful or stateless, might or might not have side effects, and can trigger external actions. All power goes to the developer. #### Support for dynamic topologies -We want the system to allow for dynamically evolving topologies. The existing systems we mentioned above are usually limited to only static topologies that are fixed at deployment time and cannot evolve at runtime. In the following example of a dataflow expression, everything is nice and simple until you need to change it. +The system should allow for dynamically evolving topologies. Existing systems mentioned above are usually limited to static topologies fixed at deployment time that cannot evolve at runtime. In the following example of a dataflow expression, everything is nice and simple until you need to change it: `` Stream.GroupBy(x=> x.key).Extract(x=>x.field).Select(x=>x+2).AverageWindow(x, 5sec).Where(x=>x > 0.8) * `` -Change the threshold condition in the filter, add statement or add another branch in the data-flow graph and produce a new output stream. In existing systems, this is not possible without tearing down the entire topology and restarting the data-flow from scratch. Practically, those systems will checkpoint the existing computation and will be able to restart from the latest checkpoint. Still, such a restart is disruptive and costly to an online service that produces results in real-time. Such a restart becomes especially impractical when we are talking about a large number of such expressions being executed with similar but different (per-user, per-device, etc.) parameters and that continually change. +Change the threshold condition in the filter, add a statement, or add another branch in the data-flow graph and produce a new output stream. In existing systems, this isn't possible without tearing down the entire topology and restarting the data flow from scratch. Practically, these systems checkpoint the existing computation and can restart from the latest checkpoint. Still, such a restart is disruptive and costly for an online service producing results in real-time. Such a restart becomes especially impractical when dealing with a large number of such expressions executed with similar but different parameters (per-user, per-device, etc.) that continually change. -We want the system to allow for evolving the stream processing graph at runtime, by adding new links or nodes to the computation graph, or by changing the processing logic within the computation nodes. +The system should allow evolving the stream processing graph at runtime by adding new links or nodes to the computation graph or changing the processing logic within the computation nodes. -#### Fine grained stream granularity +#### Fine-grained stream granularity -In the existing systems, the smallest unit of abstraction is usually the whole flow (topology). However, many of our target scenarios require an individual node/link in the topology to be a logical entity by itself. That way each entity can be potentially managed independently. For example, in the big stream topology comprising multiple links, different links can have different characteristics and can be implemented over different physical transports. Some links can go over TCP sockets, while others over reliable queues. Different links can have different delivery guarantees. Different nodes can have different checkpointing strategies, and their processing logic can be expressed in different models or even different languages. Such flexibility is usually not possible in existing systems. +In existing systems, the smallest unit of abstraction is usually the whole flow (topology). However, many target scenarios require an individual node/link in the topology to be a logical entity itself. This way, each entity can potentially be managed independently. For example, in a large stream topology comprising multiple links, different links can have different characteristics and be implemented over different physical transports. Some links might go over TCP sockets, while others use reliable queues. Different links can have different delivery guarantees. Different nodes can have different checkpointing strategies, and their processing logic can be expressed in different models or even different languages. Such flexibility usually isn't possible in existing systems. -The unit of abstraction and flexibility argument is similar to a comparison of SoA (Service Oriented Architectures) vs. Actors. Actor systems allow more flexibility since each actor is essentially an independently managed ''tiny service''. Similarly, we want the stream system to allow for such fine-grained control. +The unit of abstraction and flexibility argument is similar to comparing SoA (Service Oriented Architectures) vs. Actors. Actor systems allow more flexibility since each actor is essentially an independently managed "tiny service." Similarly, the stream system should allow for such fine-grained control. #### Distribution And of course, the system should have all the properties of a **"good distributed system"**. That includes: -1. _Scalability_ - supports large number of streams and compute elements. -2. _Elasticity_ - allows to add/remove resources to grow/shrink based on load. -3. _Reliability_ - be resilient to failures -4. _Efficiency_ - use the underlying resources efficiently -5. _Responsiveness_ - enable near real time scenarios. +1. _Scalability_: Supports a large number of streams and compute elements. +1. _Elasticity_: Allows adding/removing resources to grow/shrink based on load. +1. _Reliability_: Resilient to failures. +1. _Efficiency_: Uses underlying resources efficiently. +1. _Responsiveness_: Enables near-real-time scenarios. These were the requirements for building [**Orleans Streaming**](index.md). diff --git a/docs/orleans/tutorials-and-samples/adventure.md b/docs/orleans/tutorials-and-samples/adventure.md index 1ccfea4f566c3..897b3873486bd 100644 --- a/docs/orleans/tutorials-and-samples/adventure.md +++ b/docs/orleans/tutorials-and-samples/adventure.md @@ -1,7 +1,8 @@ --- title: Adventure game sample project description: Explore the Adventure sample project written with .NET Orleans. -ms.date: 07/03/2024 +ms.date: 03/30/2025 +ms.topic: sample --- # Adventure game sample project @@ -14,7 +15,7 @@ This sample is a simple multiplayer text adventure game inspired by old-fashione 1. Select **Browse code** to view the source code. 1. Clone the source code and build the solution. 1. Start the _AdventureServer_ first, then the _AdventureClient_. -1. You will then be prompted to enter your name on the command line. Enter it and begin the game. +1. You are then prompted to enter your name on the command line. Enter it and begin the game. For more information, see [Building the sample](/samples/dotnet/samples/orleans-text-adventure-game#building-the-sample). @@ -22,27 +23,27 @@ For more information, see [Building the sample](/samples/dotnet/samples/orleans- The _AdventureServer_ program starts by reading an _AdventureMap.json_ file. -It sets up a series of "rooms" such as a forest, beach, caves, a clearing, and so on. These locations are connected to other rooms to model the places and layout of the game. The sample configuration describes only a handful of locations. +It sets up a series of "rooms" such as a forest, beach, caves, a clearing, etc. These locations connect to other rooms to model the places and layout of the game. The sample configuration describes only a handful of locations. -Rooms can contain "things" such as keys, swords, and so on. +Rooms can contain "things" such as keys, swords, etc. -The _AdventureClient_ program sets up your player and provides a simple text-based user interface to allow you to play the game. +The _AdventureClient_ program sets up your player and provides a simple text-based user interface allowing you to play the game. You can move around rooms and interact with things using a simple command language, saying things such as "go north" or "take brass key". ## Why Orleans? -Orleans allows the game to be described via very simple C# code while allowing it to scale to a massively multiplayer game. For this motivation to be meaningful, the labyrinth of rooms needs to be very large and needs to support a large number of simultaneous players. One value of Orleans is that the service can be designed for growth, the overhead of running it at a small scale is not significant, and you can remain confident that it will scale if the need arises. +Orleans allows you to describe the game using very simple C# code while enabling it to scale to a massively multiplayer game. For this motivation to be meaningful, the labyrinth of rooms needs to be very large and support many simultaneous players. One value of Orleans is that you can design the service for growth; the overhead of running it at a small scale isn't significant, and you can remain confident it will scale if the need arises. ## How is it modeled? -Player and Rooms are modeled as grains. These grains allow you to distribute the game with each grain modeling state and functionality. +Players and Rooms are modeled as grains. These grains allow you to distribute the game, with each grain modeling state and functionality. -Things such as keys are modeled as plain old objects—they are just simple immutable data structures that move around rooms and among players; they don't need to be grains. +Things such as keys are modeled as plain old objects—they are just simple immutable data structures that move around rooms and among players; they don't need to be grains. ## Possible improvements -1. Make the map much, much, bigger -2. Make the brass key unlock something -3. Allow players to message each other -4. Make eating food and drinking water possible and meaningful +1. Make the map much, much bigger. +1. Make the brass key unlock something. +1. Allow players to message each other. +1. Make eating food and drinking water possible and meaningful. diff --git a/docs/orleans/tutorials-and-samples/custom-grain-storage.md b/docs/orleans/tutorials-and-samples/custom-grain-storage.md index d722068bca180..858125978bbcf 100644 --- a/docs/orleans/tutorials-and-samples/custom-grain-storage.md +++ b/docs/orleans/tutorials-and-samples/custom-grain-storage.md @@ -1,19 +1,20 @@ --- title: Custom grain storage sample project description: Explore a custom grain storage sample project written with .NET Orleans. -ms.date: 07/03/2024 +ms.date: 03/30/2025 +ms.topic: tutorial zone_pivot_groups: orleans-version --- # Custom grain storage -In the tutorial on declarative actor storage, we looked at allowing grains to store their state in an Azure table using one of the built-in storage providers. While Azure is a great place to squirrel away your data, there are many alternatives. There are so many that there was no way to support them all. Instead, Orleans is designed to let you easily add support for your form of storage by writing a grain storage. +In the tutorial on declarative actor storage, you learned how to allow grains to store their state in an Azure table using one of the built-in storage providers. While Azure is a great place to store your data, many alternatives exist. There are so many that supporting them all isn't feasible. Instead, Orleans is designed to let you easily add support for your preferred storage by writing a custom grain storage provider. -In this tutorial, we'll walk through how to write simple file-based grain storage. A file system is not the best place to store grains states as it is local, there can be issues with file locks and the last update date is not sufficient to prevent inconsistency. But it's an easy example to help us illustrate the implementation of a _grain storage_. +In this tutorial, you'll walk through how to write a simple file-based grain storage provider. A file system isn't the best place to store grain states because it's local, can have issues with file locks, and the last update date isn't sufficient to prevent inconsistency. However, it's an easy example to illustrate the implementation of a _grain storage_ provider. ## Get started -An Orleans grain storage is a class that implements `IGrainStorage`, which is included in [Microsoft.Orleans.Core](https://www.nuget.org/packages/Microsoft.Orleans.Core) NuGet package. It will also inherit from `ILifecycleParticipant`, which will allow you to subscribe to a particular event in the lifecycle of the silo. You start by creating a class named `FileGrainStorage`. +An Orleans grain storage provider is a class that implements `IGrainStorage`, included in the [Microsoft.Orleans.Core](https://www.nuget.org/packages/Microsoft.Orleans.Core) NuGet package. It also inherits from `ILifecycleParticipant`, allowing you to subscribe to specific events in the silo's lifecycle. Start by creating a class named `FileGrainStorage`. :::zone target="docs" pivot="orleans-7-0" @@ -72,45 +73,45 @@ public sealed class FileGrainStorage : IGrainStorage, ILifecycleParticipant: to read the state of a grain. -- : to write the state of a grain. -- : to clear the state of a grain. +- : Reads the state of a grain. +- : Writes the state of a grain. +- : Clears the state of a grain. -The method is used to subscribe to the lifecycle of the silo. +The method subscribes to the silo's lifecycle. -Before starting the implementation, you'll create an options class containing the root directory where the grain state files are persisted. For that you'll create an options file named `FileGrainStorageOptions` containing the following: +Before starting the implementation, create an options class containing the root directory where grain state files are persisted. Create an options file named `FileGrainStorageOptions` containing the following: :::code source="snippets/custom-grain-storage/FileGrainStorageOptions.cs"::: -With the options class created, you'll explore the constructor parameters of the `FileGrainStorage` class: +With the options class created, explore the constructor parameters of the `FileGrainStorage` class: -- `storageName`: to specify which grains should write using this storage, for example `[StorageProvider(ProviderName = "File")]`. -- `options`: the options class we just created. -- `clusterOptions`: the cluster options used for retrieving the `ServiceId`. +- `storageName`: Specifies which grains should use this storage provider, for example, `[StorageProvider(ProviderName = "File")]`. +- `options`: The options class just created. +- `clusterOptions`: The cluster options used for retrieving the `ServiceId`. ## Initialize the storage -To initialize the storage, you subscribe to the stage with an `onStart` function. Consider the following implementation: +To initialize the storage, subscribe to the stage with an `onStart` function. Consider the following implementation: :::code source="snippets/custom-grain-storage/FileGrainStorage.cs" id="participate"::: -The `onStart` function is used to conditionally create the root directory to store the grains states when it doesn't already exist. +The `onStart` function conditionally creates the root directory to store grain states if it doesn't already exist. -You'll also provide a common function to construct the filename ensuring uniqueness per service, grain id, and grain type: +Also, provide a common function to construct the filename, ensuring uniqueness per service, grain ID, and grain type: :::code source="snippets/custom-grain-storage/FileGrainStorage.cs" id="getkeystring"::: ## Read state -To read a grain state, you get the filename using the `GetKeyString` function and combine it with the root directory coming from the `_options` instance. +To read a grain state, get the filename using the `GetKeyString` function and combine it with the root directory from the `_options` instance. :::code source="snippets/custom-grain-storage/FileGrainStorage.cs" id="readstateasync"::: -We use the `fileInfo.LastWriteTimeUtc` as an `ETag` which will be used by other functions for inconsistency checks to prevent data loss. +Use `fileInfo.LastWriteTimeUtc` as an `ETag`, which other functions use for inconsistency checks to prevent data loss. -For the deserialization, you use the . This is important to be able to serialize/deserialize properly the state. +For deserialization, use the . This is important for correctly serializing and deserializing the state. ## Write state @@ -118,27 +119,27 @@ Writing the state is similar to reading the state. :::code source="snippets/custom-grain-storage/FileGrainStorage.cs" id="writestateasync"::: -Similar to reading state, you use the to write the state. The current `ETag` is used to check against the last updated time in the UTC of the file. If the date is different, it means that another activation of the same grain changed the state concurrently. In this situation, you'll throw an `InconsistentStateException`, which will result in the current activation being killed to prevent overwriting the state previously saved by the other activated grain. +Similar to reading state, use the to write the state. The current `ETag` checks against the file's last updated UTC time. If the date differs, it means another activation of the same grain changed the state concurrently. In this situation, throw an `InconsistentStateException`. This results in the current activation being killed to prevent overwriting the state previously saved by the other activated grain. ## Clear state -Clearing the state would be deleting the file if the file exists. +Clearing the state involves deleting the file if it exists. :::code source="snippets/custom-grain-storage/FileGrainStorage.cs" id="clearstateasync"::: -For the same reason as `WriteState`, you check for inconsistency. Before proceeding to delete the file and reset the `ETag`, you check if the current `ETag` is the same as the last write time UTC. +For the same reason as `WriteStateAsync`, check for inconsistency. Before deleting the file and resetting the `ETag`, check if the current `ETag` matches the last write time UTC. ## Put it all together -After that, you will create a factory that will allow you to scope the options to the provider name and at the same time create an instance of the `FileGrainStorage` to ease the registration to the service collection. +Next, create a factory that allows scoping the options to the provider name while creating an instance of `FileGrainStorage` to ease registration with the service collection. :::code source="snippets/custom-grain-storage/FileGrainStorageFactory.cs"::: -Lastly, to register the grain storage, you create an extension on the `ISiloBuilder` which internally registers the grain storage as a named service using , an extension provided by `Orleans.Core`. +Lastly, to register the grain storage, create an extension on `ISiloBuilder`. This extension internally registers the grain storage as a named service using , an extension provided by `Orleans.Core`. :::code source="snippets/custom-grain-storage/FileSiloBuilderExtensions.cs"::: -Our `FileGrainStorage` implements two interfaces, `IGrainStorage` and `ILifecycleParticipant`, therefore we need to register two named services for each interface: +The `FileGrainStorage` implements two interfaces, `IGrainStorage` and `ILifecycleParticipant`. Therefore, register two named services, one for each interface: ```csharp return services.AddSingletonNamedService(providerName, FileGrainStorageFactory.Create) @@ -146,11 +147,11 @@ return services.AddSingletonNamedService(providerName, FileGrainStorageFactory.C (p, n) => (ILifecycleParticipant)p.GetRequiredServiceByName(n)); ``` -This enables you to add the file storage using the extension on the `ISiloBuilder`: +This enables adding the file storage using the extension on `ISiloBuilder`: :::code source="snippets/custom-grain-storage/Program.cs"::: -Now you will be able to decorate your grains with the provider `[StorageProvider(ProviderName = "File")]` and it will store in the grain state in the root directory set in the options. Consider the full implementation of the `FileGrainStorage`: +Now you can decorate your grains with the provider `[StorageProvider(ProviderName = "File")]`, and it stores the grain state in the root directory set in the options. Consider the full implementation of `FileGrainStorage`: :::code source="snippets/custom-grain-storage/FileGrainStorage.cs"::: @@ -225,7 +226,7 @@ public class FileGrainStorage } ``` -Before starting the implementation, we create an option class containing the root directory where the grains states files will be stored. For that we will create an options file `FileGrainStorageOptions`: +Before starting the implementation, create an options class containing the root directory where grain state files are stored. Create an options file named `FileGrainStorageOptions`: ```csharp public class FileGrainStorageOptions @@ -234,20 +235,20 @@ public class FileGrainStorageOptions } ``` -The create a constructor containing two fields, `storageName` to specify which grains should write using this storage `[StorageProvider(ProviderName = "File")]` and `directory` which would be the directory where the grain states will be saved. +Create a constructor containing two fields: `storageName` to specify which grains should use this storage (`[StorageProvider(ProviderName = "File")]`) and `directory`, the directory where grain states are saved. -`IGrainFactory`, `ITypeResolver` will be used in the next section where we will initialize the storage. +`IGrainFactory` and `ITypeResolver` are used in the next section to initialize the storage. -We also take two options as an argument, our own `FileGrainStorageOptions` and the `ClusterOptions`. Those will be needed for the implementation of the storage functionalities. +Also, take two options as arguments: your own `FileGrainStorageOptions` and the `ClusterOptions`. These are needed for implementing the storage functionalities. -We also need `JsonSerializerSettings` as we are serializing and deserializing in JSON format. +You also need `JsonSerializerSettings` as you are serializing and deserializing in JSON format. > [!IMPORTANT] -> JSON is an implementation detail, it is up to the developer to decide what serialization/deserialization protocol would fit the application. Another common format is binary. +> JSON is an implementation detail. It's up to you to decide which serialization/deserialization protocol fits your application. Another common format is binary. ## Initialize the storage -To initialize the storage, we register an `Init` function on the `ApplicationServices` lifecycle. +To initialize the storage, register an `Init` function on the `ApplicationServices` lifecycle. ```csharp public void Participate(ISiloLifecycle lifecycle) @@ -259,7 +260,7 @@ public void Participate(ISiloLifecycle lifecycle) } ``` -The `Init` function is used to set the `_jsonSettings` which will be used to configure the `Json` serializer. At the same time, we create the folder to store the grains states if it does not exist yet. +The `Init` function sets the `_jsonSettings` used to configure the JSON serializer. At the same time, create the folder to store grain states if it doesn't exist yet. ```csharp private Task Init(CancellationToken ct) @@ -282,7 +283,7 @@ private Task Init(CancellationToken ct) } ``` -We also provide a common function to construct the filename ensuring uniqueness per service, grain id, and grain type. +Also, provide a common function to construct the filename, ensuring uniqueness per service, grain ID, and grain type. ```csharp private string GetKeyString(string grainType, GrainReference grainReference) @@ -293,7 +294,7 @@ private string GetKeyString(string grainType, GrainReference grainReference) ## Read state -To read a grain state, we get the filename using the function we previously defined and combine it to the root directory coming from the options. +To read a grain state, get the filename using the previously defined function and combine it with the root directory from the options. ```csharp public async Task ReadStateAsync( @@ -321,9 +322,9 @@ public async Task ReadStateAsync( } ``` -We use the `fileInfo.LastWriteTimeUtc` as an ETag which will be used by other functions for inconsistency checks to prevent data loss. +Use `fileInfo.LastWriteTimeUtc` as an ETag, which other functions use for inconsistency checks to prevent data loss. -Note that for the deserialization, we use the `_jsonSettings` which was set on the `Init` function. This is important to be able to serialize/deserialize properly the state. +Note that for deserialization, use the `_jsonSettings` set in the `Init` function. This is important for correctly serializing/deserializing the state. ## Write state @@ -360,11 +361,11 @@ public async Task WriteStateAsync( } ``` -Similar to reading state, we use `_jsonSettings` to write the state. The current ETag is used to check against the last updated time in the UTC of the file. If the date is different, it means that another activation of the same grain changed the state concurrently. In this situation, we throw an `InconsistentStateException` which will result in the current activation being killed to prevent overwriting the state previously saved by the other activated grain. +Similar to reading state, use `_jsonSettings` to write the state. The current ETag checks against the file's last updated UTC time. If the date differs, it means another activation of the same grain changed the state concurrently. In this situation, throw an `InconsistentStateException`, which results in the current activation being killed to prevent overwriting the state previously saved by the other activated grain. ## Clear state -Clearing the state would be deleting the file if the file exists. +Clearing the state involves deleting the file if it exists. ```csharp public Task ClearStateAsync( @@ -395,11 +396,11 @@ public Task ClearStateAsync( } ``` -For the same reason as `WriteState`, we check for inconsistency before proceeding to delete the file and reset the ETag, we check if the current ETag is the same as the last write time UTC. +For the same reason as `WriteStateAsync`, check for inconsistency. Before deleting the file and resetting the ETag, check if the current ETag matches the last write time UTC. ## Put it all together -After that, we will create a factory that will allow us to scope the options to the provider name and at the same time create an instance of the `FileGrainStorage` to ease the registration to the service collection. +Next, create a factory that allows scoping the options to the provider name while creating an instance of `FileGrainStorage` to ease registration with the service collection. ```csharp public static class FileGrainStorageFactory @@ -419,7 +420,7 @@ public static class FileGrainStorageFactory } ``` -Lastly, to register the grain storage, we create an extension on the `ISiloHostBuilder` which internally registers the grain storage as a named service using `.AddSingletonNamedService(...)`, an extension provided by `Orleans.Core`. +Lastly, to register the grain storage, create an extension on `ISiloHostBuilder`. This extension internally registers the grain storage as a named service using `.AddSingletonNamedService(...)`, an extension provided by `Orleans.Core`. ```csharp public static class FileSiloBuilderExtensions @@ -448,7 +449,7 @@ public static class FileSiloBuilderExtensions } ``` -Our `FileGrainStorage` implements two interfaces, `IGrainStorage` and `ILifecycleParticipant` therefore we need to register two named services for each interfaces: +The `FileGrainStorage` implements two interfaces, `IGrainStorage` and `ILifecycleParticipant`. Therefore, register two named services, one for each interface: ```csharp return services.AddSingletonNamedService(providerName, FileGrainStorageFactory.Create) @@ -457,7 +458,7 @@ return services.AddSingletonNamedService(providerName, FileGrainStorageFactory.C (s, n) => (ILifecycleParticipant)s.GetRequiredServiceByName(n)); ``` -This enables us to add the file storage using the extension on the `ISiloHostBuilder`: +This enables adding the file storage using the extension on `ISiloHostBuilder`: ```csharp var silo = new HostBuilder() @@ -472,6 +473,6 @@ var silo = new HostBuilder() .Build(); ``` -Now we will be able to decorate our grains with the provider `[StorageProvider(ProviderName = "File")]` and it will store in the grain state in the root directory set in the options. +Now you can decorate your grains with the provider `[StorageProvider(ProviderName = "File")]`, and it stores the grain state in the root directory set in the options. :::zone-end diff --git a/docs/orleans/tutorials-and-samples/index.md b/docs/orleans/tutorials-and-samples/index.md index 05392f1ddca4d..a84c8ce0baa7b 100644 --- a/docs/orleans/tutorials-and-samples/index.md +++ b/docs/orleans/tutorials-and-samples/index.md @@ -1,7 +1,8 @@ --- title: Orleans sample projects description: Explore the various sample projects written with .NET Orleans. -ms.date: 07/03/2024 +ms.date: 03/30/2025 +ms.topic: sample --- # Orleans sample projects @@ -12,137 +13,131 @@ ms.date: 07/03/2024 :::image type="content" source="https://raw.githubusercontent.com/dotnet/samples/main/orleans/HelloWorld/code.png" alt-text="Sample code for the Hello World Orleans app."::: -A *Hello, World!* application that demonstrates how to create and use your first grains. +A *Hello, World!* application demonstrating how to create and use your first grains. ### Hello World demonstrates -* How to get started with Orleans -* How to define and implement grain interface -* How to get a reference to a grain and call a grain +- How to get started with Orleans +- How to define and implement a grain interface +- How to get a reference to a grain and call it ## [Shopping Cart](/samples/dotnet/samples/orleans-shopping-cart-app-sample) :::image type="content" source="https://raw.githubusercontent.com/dotnet/samples/main/orleans/ShoppingCart/media/shopping-cart.png" alt-text="Screen capture from the Shopping Cart Orleans sample app."::: -A canonical shopping cart sample application, built using Microsoft Orleans. This app shows the following features: +A canonical shopping cart sample application built using Microsoft Orleans. This app shows the following features: -* **Shopping cart**: A simple shopping cart application that uses Orleans for its cross-platform framework support, and its scalable distributed applications capabilities. +- **Shopping cart**: A simple shopping cart application using Orleans for its cross-platform framework support and scalable distributed application capabilities. - * **Inventory management**: Edit and/or create product inventory. - * **Shop inventory**: Explore purchasable products and add them to your cart. - * **Cart**: View a summary of all the items in your cart, and manage these items; either removing or changing the quantity of each item. + - **Inventory management**: Edit and/or create product inventory. + - **Shop inventory**: Explore purchasable products and add them to your cart. + - **Cart**: View a summary of all items in your cart and manage these items by removing or changing the quantity of each item. ### Shopping cart demonstrates -* How to create a distributed shopping cart experience -* How to manage grain persistence as it relates to live inventory updates -* How to expose user-specific items that span multiple clients +- How to create a distributed shopping cart experience +- How to manage grain persistence as it relates to live inventory updates +- How to expose user-specific items spanning multiple clients ## [Adventure](/samples/dotnet/samples/orleans-text-adventure-game) :::image type="content" source="https://raw.githubusercontent.com/dotnet/samples/main/orleans/Adventure/assets/BoxArt.jpg" alt-text="Cover art for the Adventure Orleans app."::: -Before there were graphical user interfaces, before the era of game consoles and massive-multiplayer games, there were VT100 terminals and there was [Colossal Cave Adventure](https://en.wikipedia.org/wiki/Colossal_Cave_Adventure), [Zork](https://en.wikipedia.org/wiki/Zork), and [Microsoft Adventure](https://en.wikipedia.org/wiki/Microsoft_Adventure). -Possibly bland by today's standards, back then it was a magical world of monsters, chirping birds, and things you could pick up. -It's the inspiration for this sample. +Before graphical user interfaces, game consoles, and massive-multiplayer games, there were VT100 terminals and games like [Colossal Cave Adventure](https://en.wikipedia.org/wiki/Colossal_Cave_Adventure), [Zork](https://en.wikipedia.org/wiki/Zork), and [Microsoft Adventure](https://en.wikipedia.org/wiki/Microsoft_Adventure). Possibly bland by today's standards, back then it was a magical world of monsters, chirping birds, and things you could pick up. This sample draws inspiration from those games. ### Adventure demonstrates -* How to structure an application (in this case, a game) using grains -* How to connect an external client to an Orleans cluster (`ClientBuilder`) +- How to structure an application (in this case, a game) using grains +- How to connect an external client to an Orleans cluster (`ClientBuilder`) ## [Chirper](/samples/dotnet/samples/orleans-chirper-social-media-sample-app) :::image type="content" source="https://raw.githubusercontent.com/dotnet/samples/main/orleans/Chirper/screenshot.png" alt-text="Sample code for the Chirper Orleans app."::: -A social network pub/sub system, with short text messages being sent between users. -Publishers send out short *"Chirp"* messages (not to be confused with *"Tweets"*, for various legal reasons) to any other users that are following them. +A social network pub/sub system where users send short text messages to each other. Publishers send out short *"Chirp"* messages (not to be confused with *"Tweets"* for various legal reasons) to any other users following them. ### Chirper demonstrates -* How to build a simplified social media / social network application using Orleans -* How to store state within a grain using grain persistence (`IPersistentState`) -* Grains that implement multiple grain interfaces -* Reentrant grains, which allow for multiple grain calls to be executed concurrently, in a single-threaded, interleaving fashion -* Using a *grain observer* (`IGrainObserver`) to receive push notifications from grains +- How to build a simplified social media / social network application using Orleans +- How to store state within a grain using grain persistence (`IPersistentState`) +- Grains implementing multiple grain interfaces +- Reentrant grains, allowing multiple grain calls to execute concurrently in a single-threaded, interleaving fashion +- Using a *grain observer* (`IGrainObserver`) to receive push notifications from grains ## [GPS Tracker](/samples/dotnet/samples/orleans-gps-device-tracker-sample) :::image type="content" source="https://raw.githubusercontent.com/dotnet/samples/main/orleans/GPSTracker/screenshot.jpeg" alt-text="Sample code for the GPS Orleans app."::: -A service for tracking GPS-equipped [IoT](/dotnet/iot) IoT devices on a map. -Device locations are updated in near-real-time using SignalR and hence this sample demonstrates one approach to integrating Orleans with SignalR. The device updates originate from a *device gateway*, which is implemented using a separate process that connects to the main service and simulates several devices moving in a pseudorandom fashion around an area of San Francisco. +A service for tracking GPS-equipped [IoT](/dotnet/iot) devices on a map. Device locations update in near-real-time using SignalR, demonstrating one approach to integrating Orleans with SignalR. The device updates originate from a *device gateway*, implemented using a separate process that connects to the main service and simulates several devices moving pseudorandomly around an area of San Francisco. ### GPS Tracker demonstrates -* How to use Orleans to build an [Internet of Things](/dotnet/iot) application -* How Orleans can be co-hosted and integrated with [ASP.NET Core SignalR](/aspnet/core/signalr/introduction) -* How to broadcast real-time updates from a grain to a set of clients using Orleans and SignalR +- How to use Orleans to build an [Internet of Things](/dotnet/iot) application +- How Orleans can be co-hosted and integrated with [ASP.NET Core SignalR](/aspnet/core/signalr/introduction) +- How to broadcast real-time updates from a grain to a set of clients using Orleans and SignalR ## [HanBaoBao](https://github.com/ReubenBond/hanbaobao-web) :::image type="content" source="https://raw.githubusercontent.com/ReubenBond/hanbaobao-web/main/assets/demo-1.png" alt-text="HanBaoBao - Orleans sample application screen capture."::: -An English-Mandarin dictionary Web application demonstrating deployment to Kubernetes, fan-out grain calls, and request throttling. +An English-Mandarin dictionary web application demonstrating deployment to Kubernetes, fan-out grain calls, and request throttling. ### HanBaoBao demonstrates -* How to build a realistic application using Orleans -* How to deploy an Orleans-based application to Kubernetes -* How to integrate Orleans with ASP.NET Core and a [*Single-page Application*](https://en.wikipedia.org/wiki/Single-page_application) JavaScript framework ([Vue.js](https://vuejs.org/)) -* How to implement leaky-bucket request throttling -* How to load and query data from a database -* How to cache results lazily and temporarily -* How to fan out requests to many grains and collect the results +- How to build a realistic application using Orleans +- How to deploy an Orleans-based application to Kubernetes +- How to integrate Orleans with ASP.NET Core and a [*Single-page Application*](https://en.wikipedia.org/wiki/Single-page_application) JavaScript framework ([Vue.js](https://vuejs.org/)) +- How to implement leaky-bucket request throttling +- How to load and query data from a database +- How to cache results lazily and temporarily +- How to fan out requests to many grains and collect the results ## [Presence Service](/samples/dotnet/samples/orleans-gaming-presence-service-sample) :::image type="content" source="https://raw.githubusercontent.com/dotnet/samples/main/orleans/Presence/screenshot.png" alt-text="Output from the Presence Service Orleans app."::: -A gaming presence service, similar to one of the Orleans-based services built for [Halo](https://www.xbox.com/games/halo). -A presence service tracks players and game sessions in near-real-time. +A gaming presence service, similar to one of the Orleans-based services built for [Halo](https://www.xbox.com/games/halo). A presence service tracks players and game sessions in near-real-time. ### Presence Service demonstrates -* A simplified version of a real-world use of Orleans -* Using a *grain observer* (`IGrainObserver`) to receive push notifications from grains +- A simplified version of a real-world use of Orleans +- Using a *grain observer* (`IGrainObserver`) to receive push notifications from grains ## [Tic Tac Toe](/samples/dotnet/samples/orleans-tictactoe-web-based-game) :::image type="content" source="https://raw.githubusercontent.com/dotnet/samples/main/orleans/TicTacToe/logo.png" alt-text="Logo from the Tic Tac Toe Orleans sample app."::: -A Web-based [Tic-tac-toe](https://en.wikipedia.org/wiki/Tic-tac-toe) game using ASP.NET MVC, JavaScript, and Orleans. +A web-based [Tic-tac-toe](https://en.wikipedia.org/wiki/Tic-tac-toe) game using ASP.NET MVC, JavaScript, and Orleans. ### Tic Tac Toe demonstrates -* How to build an online game using Orleans -* How to build a basic game lobby system -* How to access Orleans grains from an ASP.NET Core MVC application +- How to build an online game using Orleans +- How to build a basic game lobby system +- How to access Orleans grains from an ASP.NET Core MVC application ## [Voting](/samples/dotnet/samples/orleans-voting-sample-app-on-kubernetes) :::image type="content" source="https://raw.githubusercontent.com/dotnet/samples/main/orleans/Voting/screenshot.png" alt-text="Screen capture from Voting Orleans sample app."::: -A Web application for voting on a set of choices. This sample demonstrates deployment to Kubernetes. -The application uses [.NET generic host](../../core/extensions/generic-host.md) to co-host [ASP.NET Core](/aspnet/core) and Orleans as well as the [Orleans Dashboard](https://github.com/OrleansContrib/OrleansDashboard) together in the same process. +A web application for voting on a set of choices. This sample demonstrates deployment to Kubernetes. The application uses the [.NET generic host](../../core/extensions/generic-host.md) to co-host [ASP.NET Core](/aspnet/core) and Orleans, as well as the [Orleans Dashboard](https://github.com/OrleansContrib/OrleansDashboard), together in the same process. :::image type="content" source="https://raw.githubusercontent.com/dotnet/samples/main/orleans/Voting/dashboard.png" alt-text="The Orleans dashboard running as part of the Voting sample app."::: ### Voting demonstrates -* How to deploy an Orleans-based application to Kubernetes -* How to configure the [Orleans Dashboard](https://github.com/OrleansContrib/OrleansDashboard) +- How to deploy an Orleans-based application to Kubernetes +- How to configure the [Orleans Dashboard](https://github.com/OrleansContrib/OrleansDashboard) ## [Chat Room](/samples/dotnet/samples/orleans-chat-room-sample) :::image type="content" source="https://raw.githubusercontent.com/dotnet/samples/main/orleans/ChatRoom/screenshot.png" alt-text="Sample output from the running Chat Room sample Orleans app."::: -A terminal-based chat application built using [Orleans Streams](https://dotnet.github.io/orleans/docs/streaming/index.html). +A terminal-based chat application built using [Orleans Streams](../streaming/index.md). ### Chat Room demonstrates -* How to build a chat application using Orleans -* How to use [Orleans Streams](https://dotnet.github.io/orleans/docs/streaming/index.html) +- How to build a chat application using Orleans +- How to use [Orleans Streams](../streaming/index.md) ## [Bank Account](/samples/dotnet/samples/orleans-bank-account-acid-transactions) @@ -152,33 +147,30 @@ Simulates bank accounts, using ACID transactions to transfer random amounts betw ### Bank Account demonstrates -* How to use Orleans Transactions to safely perform operations involving multiple stateful grains with ACID guarantees and serializable isolation. +- How to use Orleans Transactions to safely perform operations involving multiple stateful grains with ACID guarantees and serializable isolation. ## [Blazor Server](/samples/dotnet/samples/orleans-aspnet-core-blazor-server-sample) and [Blazor WebAssembly](/samples/dotnet/samples/orleans-aspnet-core-blazor-wasm-sample) :::image type="content" source="https://raw.githubusercontent.com/dotnet/samples/main/orleans/Blazor/BlazorServer/screenshot.png" alt-text="Blazor Orleans sample app screen capture"::: -These two Blazor samples are based on the [Blazor introductory tutorials](https://dotnet.microsoft.com/learn/aspnet/blazor-tutorial/intro), adapted for use with Orleans. -The [Blazor WebAssembly](https://github.com/dotnet/samples/tree/main/orleans/Blazor/BlazorWasm) sample uses the [Blazor WebAssembly hosting model](/aspnet/core/blazor/hosting-models#blazor-webassembly). -The [Blazor Server](https://github.com/dotnet/samples/tree/main/orleans/Blazor/BlazorServer) sample uses the [Blazor Server hosting model](/aspnet/core/blazor/hosting-models#blazor-server). -They include an interactive counter, a TODO list, and a Weather service. +These two Blazor samples are based on the [Blazor introductory tutorials](https://dotnet.microsoft.com/learn/aspnet/blazor-tutorial/intro), adapted for use with Orleans. The [Blazor WebAssembly](https://github.com/dotnet/samples/tree/main/orleans/Blazor/BlazorWasm) sample uses the [Blazor WebAssembly hosting model](/aspnet/core/blazor/hosting-models#blazor-webassembly). The [Blazor Server](https://github.com/dotnet/samples/tree/main/orleans/Blazor/BlazorServer) sample uses the [Blazor Server hosting model](/aspnet/core/blazor/hosting-models#blazor-server). They include an interactive counter, a TODO list, and a Weather service. ### Blazor sample apps demonstrate -* How to integrate ASP.NET Core Blazor Server with Orleans -* How to integrate ASP.NET Core Blazor WebAssembly (WASM) with Orleans +- How to integrate ASP.NET Core Blazor Server with Orleans +- How to integrate ASP.NET Core Blazor WebAssembly (WASM) with Orleans ## [Stocks](/samples/dotnet/samples/orleans-stocks-sample-app) :::image type="content" source="https://raw.githubusercontent.com/dotnet/samples/main/orleans/Stocks/screenshot.png" alt-text="Output from the running Stocks client sample Orleans app."::: -A stock price application that fetches prices from a remote service using an HTTP call and caches prices temporarily in a grain. A [BackgroundService](../../core/extensions/workers.md) periodically polls for updated stock prices from various `StockGrain` grains that correspond to a set of stock symbols. +A stock price application that fetches prices from a remote service using an HTTP call and caches prices temporarily in a grain. A [BackgroundService](../../core/extensions/workers.md) periodically polls for updated stock prices from various `StockGrain` grains corresponding to a set of stock symbols. ### Stocks sample app demonstrates -* How to use Orleans from within a [BackgroundService](../../core/extensions/workers.md). -* How to use timers within a grain. -* How to make external service calls using .NET's `HttpClient` and cache the results within a grain. +- How to use Orleans from within a [BackgroundService](../../core/extensions/workers.md). +- How to use timers within a grain. +- How to make external service calls using .NET's `HttpClient` and cache the results within a grain. ## [Transport Layer Security](/samples/dotnet/samples/orleans-transport-layer-security-tls) @@ -188,7 +180,7 @@ A *Hello, World!* application configured to use mutual [*Transport Layer Securit ### Transport Layer Security demonstrates -* How to configure mutual-TLS (mTLS) authentication for Orleans +- How to configure mutual-TLS (mTLS) authentication for Orleans ## [Visual Basic Hello World](/samples/dotnet/samples/orleans-vb-sample/) @@ -196,7 +188,7 @@ A *Hello, World!* application using Visual Basic. ### Visual Basic Hello World demonstrates -* How to develop Orleans-based applications using Visual Basic +- How to develop Orleans-based applications using Visual Basic ## [F# Hello World](/samples/dotnet/samples/orleans-fsharp-sample) @@ -204,7 +196,7 @@ A *Hello, World!* application using F#. ### F# Hello World demonstrates -* How to develop Orleans-based applications using F# +- How to develop Orleans-based applications using F# ## [Streaming: Pub/Sub Streams over Azure Event Hubs](/samples/dotnet/samples/orleans-streaming-samples) @@ -212,17 +204,17 @@ An application using Orleans Streams with [Azure Event Hubs](https://azure.micro ### Pub/Sub Streams demonstrates -* How to use [Orleans Streams](https://dotnet.github.io/orleans/docs/streaming/index.html) -* How to use the `[ImplicitStreamSubscription(namespace)]` attribute to implicitly subscribe a grain to the stream with the corresponding ID -* How to configure Orleans Streams for use with [Azure Event Hubs](https://azure.microsoft.com/services/event-hubs/) +- How to use [Orleans Streams](../streaming/index.md) +- How to use the `[ImplicitStreamSubscription(namespace)]` attribute to implicitly subscribe a grain to the stream with the corresponding ID +- How to configure Orleans Streams for use with [Azure Event Hubs](https://azure.microsoft.com/services/event-hubs/) ## [Streaming: Custom Data Adapter](/samples/dotnet/samples/orleans-streaming-samples) -An application using Orleans Streams with a non-Orleans publisher pushing to a stream that a grain consumes via a *custom data adapter* that tells Orleans how to interpret stream messages. +An application using Orleans Streams with a non-Orleans publisher pushing to a stream that a grain consumes via a *custom data adapter*, telling Orleans how to interpret stream messages. ### Custom Data Adapter demonstrates -* How to use [Orleans Streams](../streaming/index.md) -* How to use the `[ImplicitStreamSubscription(namespace)]` attribute to implicitly subscribe a grain to the stream with the corresponding ID -* How to configure Orleans Streams for use with [Azure Event Hubs](https://azure.microsoft.com/services/event-hubs/) -* How to consume stream messages published by non-Orleans publishers by providing a custom `EventHubDataAdapter` implementation (a custom data adapter) +- How to use [Orleans Streams](../streaming/index.md) +- How to use the `[ImplicitStreamSubscription(namespace)]` attribute to implicitly subscribe a grain to the stream with the corresponding ID +- How to configure Orleans Streams for use with [Azure Event Hubs](https://azure.microsoft.com/services/event-hubs/) +- How to consume stream messages published by non-Orleans publishers by providing a custom `EventHubDataAdapter` implementation (a custom data adapter) diff --git a/docs/orleans/tutorials-and-samples/overview-helloworld.md b/docs/orleans/tutorials-and-samples/overview-helloworld.md index af3f70e160cc4..04de21ce993dc 100644 --- a/docs/orleans/tutorials-and-samples/overview-helloworld.md +++ b/docs/orleans/tutorials-and-samples/overview-helloworld.md @@ -1,25 +1,24 @@ --- title: "Tutorial: Hello world" description: Explore the hello world tutorial project written with .NET Orleans. -ms.date: 07/03/2024 +ms.date: 03/30/2025 +ms.topic: tutorial --- # Tutorial: Hello world This overview ties into the [Hello World sample application](https://github.com/dotnet/samples/tree/main/orleans/HelloWorld). -The main concepts of Orleans involve a silo, a client, and one or more grains. -Creating an Orleans app involves configuring the silo, configuring the client, and writing the grains. +The main concepts of Orleans involve a silo, a client, and one or more grains. Creating an Orleans app involves configuring the silo, configuring the client, and writing the grains. ## Configure the silo -Silos are configured programmatically via `SiloHostBuilder` and several supplemental option classes. -A list of all of the options can be found at [List of options classes](../host/configuration-guide/list-of-options-classes.md). +Configure silos programmatically via `ISiloBuilder` and several supplemental option classes. You can find a list of all options at [List of options classes](../host/configuration-guide/list-of-options-classes.md). ```csharp -static async Task StartSilo() +static async Task StartSilo(string[] args) { - var builder = new HostBuilder() + var builder = Host.CreateApplicationBuilder(args) UseOrleans(c => { c.UseLocalhostClustering() @@ -42,18 +41,18 @@ static async Task StartSilo() } ``` -| Option | Used for | -|--|--| +| Option | Used for | +| --------------------------- | -------- | | `.UseLocalhostClustering()` | Configures the client to connect to a silo on the localhost. | -| `ClusterOptions` | ClusterId is the name for the Orleans cluster must be the same for silo and client so they can talk to each other. ServiceId is the ID used for the application and it must not change across deployments | -| `EndpointOptions` | This tells the silo where to listen. For this example, we are using a `loopback`. | +| `ClusterOptions` | `ClusterId` is the name for the Orleans cluster; it must be the same for the silo and client so they can communicate. `ServiceId` is the ID used for the application and must not change across deployments. | +| `EndpointOptions` | Tells the silo where to listen. For this example, use `loopback`. | | `ConfigureApplicationParts` | Adds the grain class and interface assembly as application parts to your Orleans application. | -After loading the configurations, the SiloHost is built and then started asynchronously. +After loading the configurations, build the `ISiloHost` and then start it asynchronously. -## Configuring the client +## Configure the client -Similar to the silo, the client is configured via `ClientBuilder` and a similar collection of option classes. +Similar to the silo, configure the client via `IClientBuilder` and a similar collection of option classes. ```csharp static async Task StartClientWithRetries() @@ -76,17 +75,17 @@ static async Task StartClientWithRetries() ``` | Option | Used for | -|-----------------------------|------------------------| +| --------------------------- | ---------------------- | | `.UseLocalhostClustering()` | Same as for `SiloHost` | | `ClusterOptions` | Same as for `SiloHost` | -A more in-depth guide to configuring your client can be found [in the Client Configuration section of the Configuration Guide](../host/configuration-guide/client-configuration.md). +Find a more in-depth guide to configuring your client in the [Client configuration](../host/configuration-guide/client-configuration.md) section of the Configuration Guide. -## Writing a grain +## Write a grain -Grains are the key primitives of the Orleans programming model. Grains are the building blocks of an Orleans application, they are atomic units of isolation, distribution, and persistence. Grains are objects that represent application entities. Just like in the classic Object-Oriented Programming, a grain encapsulates the state of an entity and encodes its behavior in the code logic. Grains can hold references to each other and interact by invoking each other's methods exposed via interfaces. +Grains are the key primitives of the Orleans programming model. They are the building blocks of an Orleans application, serving as atomic units of isolation, distribution, and persistence. Grains are objects representing application entities. Just like in classic Object-Oriented Programming, a grain encapsulates an entity's state and encodes its behavior in code logic. Grains can hold references to each other and interact by invoking methods exposed via interfaces. -You can read more about them in the [Core Concepts section of the Orleans documentation](../grains/index.md). +Read more about them in the [Grains](../grains/index.md) section of the Orleans documentation. This is the main body of code for the Hello World grain: @@ -103,7 +102,7 @@ public class HelloGrain : Orleans.Grain, IHello } ``` -A grain class implements one or more grain interfaces. For more information, see the [Grains section](../grains/index.md). +A grain class implements one or more grain interfaces. For more information, see the [Grains](../grains/index.md) section. ```csharp namespace HelloWorld.Interfaces; @@ -116,8 +115,7 @@ public interface IHello : Orleans.IGrainWithIntegerKey ## How the parts work together -This programming model is built as part of our core concept of distributed Object-Oriented Programming. SiloHost is started first. -Then, the OrleansClient program is started. The Main method of OrleansClient calls the method that starts the client, `StartClientWithRetries()`. The client is passed to the `DoClientWork()` method. +This programming model builds on the core concept of distributed Object-Oriented Programming. Start the `ISiloHost` first. Then, start the `OrleansClient` program. The `Main` method of `OrleansClient` calls the method that starts the client, `StartClientWithRetries()`. Pass the client to the `DoClientWork()` method. ```csharp static async Task DoClientWork(IClusterClient client) @@ -128,8 +126,8 @@ static async Task DoClientWork(IClusterClient client) } ``` -At this point, `OrleansClient` creates a reference to the `IHello` grain and calls its `SayHello()` method through its interface, `IHello`. This call activates the grain in the silo. `OrleansClient` sends a greeting to the activated grain. The grain returns the greeting as a response to `OrleansClient`, which `OrleansClient` displays on the console. +At this point, `OrleansClient` creates a reference to the `IHello` grain and calls its `SayHello()` method via the `IHello` interface. This call activates the grain in the silo. `OrleansClient` sends a greeting to the activated grain. The grain returns the greeting as a response to `OrleansClient`, which then displays it on the console. -## Running the sample app +## Run the sample app To run the sample app, refer to the [Readme](https://github.com/dotnet/samples/tree/main/orleans/HelloWorld). diff --git a/docs/orleans/tutorials-and-samples/tutorial-1.md b/docs/orleans/tutorials-and-samples/tutorial-1.md index 1948afe432b69..afc57c0896119 100644 --- a/docs/orleans/tutorials-and-samples/tutorial-1.md +++ b/docs/orleans/tutorials-and-samples/tutorial-1.md @@ -1,14 +1,15 @@ --- title: Minimal Orleans app sample project description: Explore the minimal Orleans app sample project. -ms.date: 07/03/2024 +ms.date: 03/30/2025 +ms.topic: tutorial --- # Tutorial: Create a minimal Orleans application -In this tutorial, you follow step-by-step instructions to create foundational moving parts common to most Orleans applications. It's designed to be self-contained and minimalistic. +In this tutorial, follow step-by-step instructions to create the foundational moving parts common to most Orleans applications. It's designed to be self-contained and minimalistic. -This tutorial lacks appropriate error handling and other essential code that would be useful for a production environment. However, it should help you get a hands-on understanding of the common app structure for Orleans and allow you to focus your continued learning on the parts most relevant to you. +This tutorial lacks appropriate error handling and other essential code useful for a production environment. However, it should help you gain a hands-on understanding of the common Orleans app structure and allow you to focus your continued learning on the parts most relevant to you. ## Prerequisites @@ -17,7 +18,7 @@ This tutorial lacks appropriate error handling and other essential code that wou ## Project setup -For this tutorial you're going to create four projects as part of the same solution: +For this tutorial, create four projects as part of the same solution: - Library to contain the grain interfaces. - Library to contain the grain classes. @@ -26,9 +27,9 @@ For this tutorial you're going to create four projects as part of the same solut ### Create the structure in Visual Studio -Replace the default code with the code given for each project. +Replace the default code with the code provided for each project. -1. Start by creating a Console App project in a new solution. Call the project part silo and name the solution `OrleansHelloWorld`. For more information on creating a console app, see [Tutorial: Create a .NET console application using Visual Studio](../../core/tutorials/with-visual-studio.md). +1. Start by creating a Console App project in a new solution. Call the project `Silo` and name the solution `OrleansHelloWorld`. For more information on creating a console app, see [Tutorial: Create a .NET console application using Visual Studio](../../core/tutorials/with-visual-studio.md). 1. Add another Console App project and name it `Client`. 1. Add a Class Library and name it `GrainInterfaces`. For information on creating a class library, see [Tutorial: Create a .NET class library using Visual Studio](../../core/tutorials/library-with-visual-studio.md). 1. Add another Class Library and name it `Grains`. @@ -53,23 +54,23 @@ Replace the default code with the code given for each project. | Grain Interfaces | `Microsoft.Orleans.Sdk` | | Grains | `Microsoft.Orleans.Sdk`
`Microsoft.Extensions.Logging.Abstractions` | -`Microsoft.Orleans.Server`, `Microsoft.Orleans.Client` and `Microsoft.Orleans.Sdk` are metapackages that bring dependencies that you'll most likely need on the Silo and client. For more information on adding package references, see [dotnet package add](../../core/tools/dotnet-package-add.md) or [Install and manage packages in Visual Studio using the NuGet Package Manager](/nuget/consume-packages/install-use-packages-visual-studio). +`Microsoft.Orleans.Server`, `Microsoft.Orleans.Client`, and `Microsoft.Orleans.Sdk` are metapackages that bring dependencies you'll most likely need on the Silo and client. For more information on adding package references, see [dotnet package add](../../core/tools/dotnet-package-add.md) or [Install and manage packages in Visual Studio using the NuGet Package Manager](/nuget/consume-packages/install-use-packages-visual-studio). ## Define a grain interface -In the **GrainInterfaces** project, add an _IHello.cs_ code file and define the following `IHello` interface in it: +In the **GrainInterfaces** project, add an _IHello.cs_ code file and define the following `IHello` interface: :::code source="snippets/minimal/GrainInterfaces/IHello.cs"::: ## Define a grain class -In the **Grains** project, add a _HelloGrain.cs_ code file and define the following class in it: +In the **Grains** project, add a _HelloGrain.cs_ code file and define the following class: :::code source="snippets/minimal/Grains/HelloGrain.cs"::: -### Create the Silo +### Create the silo -To create the Silo project, add code to initialize a server that hosts and runs the grains—a silo. You use the localhost clustering provider, which allows you to run everything locally, without a dependency on external storage systems. For more information, see [Local Development Configuration](../host/configuration-guide/local-development-configuration.md). In this example, you run a cluster with a single silo in it. +To create the Silo project, add code to initialize a server that hosts and runs the grains—a silo. Use the localhost clustering provider, which allows running everything locally without depending on external storage systems. For more information, see [Local Development Configuration](../host/configuration-guide/local-development-configuration.md). In this example, run a cluster with a single silo. Add the following code to _Program.cs_ of the **Silo** project: @@ -78,18 +79,18 @@ Add the following code to _Program.cs_ of the **Silo** project: The preceding code: - Configures the to use Orleans with the method. -- Specifies that the localhost clustering provider should be used with the method. -- Runs the `host` and waits for the process to terminate, by listening for Ctrl+C or `SIGTERM`. +- Specifies using the localhost clustering provider with the method. +- Runs the `host` and waits for the process to terminate by listening for Ctrl+C or `SIGTERM`. ### Create the client -Finally, you need to configure a client for communicating with the grains, connect it to the cluster (with a single silo in it), and invoke the grain. The clustering configuration must match the one you used for the silo. For more information, see [Clusters and Clients](../host/client.md). +Finally, configure a client to communicate with the grains, connect it to the cluster (with a single silo), and invoke the grain. The clustering configuration must match the one used for the silo. For more information, see [Clusters and Clients](../host/client.md). :::code source="snippets/minimal/Client/Program.cs"::: ## Run the application -Build the solution and run the **Silo**. After you get the confirmation message that the **Silo** is running, run the **Client**. +Build the solution and run the **Silo**. After receiving the confirmation message that the **Silo** is running, run the **Client**. To start the Silo from the command line, run the following command from the directory containing the Silo's project file: @@ -97,7 +98,7 @@ To start the Silo from the command line, run the following command from the dire dotnet run ``` -You'll see numerous outputs as part of the startup of the Silo. After seeing the following message you're ready to run the client: +You'll see numerous outputs as part of the Silo startup. After seeing the following message, you're ready to run the client: ```Output Application started. Press Ctrl+C to shut down. From 37fd3676d1f291fadc7e70153dca41617a2b3ebe Mon Sep 17 00:00:00 2001 From: David Pine Date: Wed, 28 May 2025 14:06:06 -0500 Subject: [PATCH 08/13] Short-lived hosted scenario. (#46460) * Fixes #45902 * Did I not save the code? * Update docs/core/extensions/workers.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Move and correct code ref --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../hosts/ShortLived.App/JobRunner.cs | 17 ++++++++++++ .../snippets/hosts/ShortLived.App/Program.cs | 25 ++++++++++++++++++ .../ShortLived.App/ShortLived.App.csproj | 13 ++++++++++ docs/core/extensions/workers.md | 26 ++++++++++++++++--- 4 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 docs/core/extensions/snippets/hosts/ShortLived.App/JobRunner.cs create mode 100644 docs/core/extensions/snippets/hosts/ShortLived.App/Program.cs create mode 100644 docs/core/extensions/snippets/hosts/ShortLived.App/ShortLived.App.csproj diff --git a/docs/core/extensions/snippets/hosts/ShortLived.App/JobRunner.cs b/docs/core/extensions/snippets/hosts/ShortLived.App/JobRunner.cs new file mode 100644 index 0000000000000..b9eda9d2c3a4c --- /dev/null +++ b/docs/core/extensions/snippets/hosts/ShortLived.App/JobRunner.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Logging; + +internal sealed class JobRunner(ILogger logger) +{ + public async Task RunAsync() + { + logger.LogInformation("Starting job..."); + + // Simulate work + await Task.Delay(1000); + + // Simulate failure + // throw new InvalidOperationException("Something went wrong!"); + + logger.LogInformation("Job completed successfully."); + } +} diff --git a/docs/core/extensions/snippets/hosts/ShortLived.App/Program.cs b/docs/core/extensions/snippets/hosts/ShortLived.App/Program.cs new file mode 100644 index 0000000000000..56ad5b57adfb5 --- /dev/null +++ b/docs/core/extensions/snippets/hosts/ShortLived.App/Program.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +var builder = Host.CreateApplicationBuilder(args); +builder.Services.AddSingleton(); + +using var host = builder.Build(); + +try +{ + var runner = host.Services.GetRequiredService(); + + await runner.RunAsync(); + + return 0; // success +} +catch (Exception ex) +{ + var logger = host.Services.GetRequiredService>(); + + logger.LogError(ex, "Unhandled exception occurred during job execution."); + + return 1; // failure +} diff --git a/docs/core/extensions/snippets/hosts/ShortLived.App/ShortLived.App.csproj b/docs/core/extensions/snippets/hosts/ShortLived.App/ShortLived.App.csproj new file mode 100644 index 0000000000000..5d0e643bff550 --- /dev/null +++ b/docs/core/extensions/snippets/hosts/ShortLived.App/ShortLived.App.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + true + ShortLived.App + + + + + + diff --git a/docs/core/extensions/workers.md b/docs/core/extensions/workers.md index 409aae15bf9f6..ad427cebc7129 100644 --- a/docs/core/extensions/workers.md +++ b/docs/core/extensions/workers.md @@ -3,7 +3,7 @@ title: Worker Services description: Learn how to implement a custom IHostedService and use existing implementations in C#. Discover various worker implementations, templates, and service patterns. author: IEvangelist ms.author: dapine -ms.date: 12/13/2023 +ms.date: 05/28/2025 ms.topic: overview --- @@ -136,14 +136,14 @@ These two methods serve as *lifecycle* methods - they're called during host star ## Signal completion -In most common scenarios, you don't need to explicitly signal the completion of a hosted service. When the host starts the services, they're designed to run until the host is stopped. In some scenarios, however, you may need to signal the completion of the entire host application when the service completes. To signal the completion, consider the following `Worker` class: +In most common scenarios, you don't need to explicitly signal the completion of a hosted service. When the host starts the services, they're designed to run until the host is stopped. In some scenarios, however, you might need to signal the completion of the entire host application when the service completes. To signal the completion, consider the following `Worker` class: :::code source="snippets/workers/signal-completion-service/App.SignalCompletionService/Worker.cs"::: -In the preceding code, the `ExecuteAsync` method doesn't loop, and when it's complete it calls . +In the preceding code, the method doesn't loop, and when it's complete it calls . > [!IMPORTANT] -> This will signal to the host that it should stop, and without this call to `StopApplication` the host will continue to run indefinitely. +> This will signal to the host that it should stop, and without this call to `StopApplication` the host will continue to run indefinitely. If you intend to run a short-lived hosted service (run once scenario), and you want to use the Worker template, you must call `StopApplication` to signal the host to stop. For more information, see: @@ -151,6 +151,24 @@ For more information, see: - [.NET Generic Host: Host shutdown](generic-host.md#host-shutdown) - [.NET Generic Host: Hosting shutdown process](generic-host.md#hosting-shutdown-process) +### Alternative approach + +For a short-lived app that needs dependency injection, logging, and configuration, use the [.NET Generic Host](generic-host.md) instead of the Worker template. This lets you use these features without the `Worker` class. A simple example of a short-lived app using the generic host might define a project file like the following: + +:::code language="xml" source="snippets/hosts/ShortLived.App/ShortLived.App.csproj"::: + +It's `Program` class might look something like the following: + +:::code language="csharp" source="snippets/hosts/ShortLived.App/Program.cs"::: + +The preceding code creates a `JobRunner` service, which is a custom class that contains the logic for the job to run. The `RunAsync` method is called on the `JobRunner`, and if it completes successfully, the app returns `0`. If an unhandled exception occurs, it logs the error and returns `1`. + +In this simple scenario, the `JobRunner` class could look like this: + +:::code language="csharp" source="snippets/hosts/ShortLived.App/JobRunner.cs"::: + +You'd obviously need to add real logic to the `RunAsync` method, but this example demonstrates how to use the generic host for a short-lived app without the need for a `Worker` class, and without the need for explicitly signaling the completion of the host. + ## See also - subclass tutorials: From 593f88e78b1500ce2377a6818ef9bae5baab784f Mon Sep 17 00:00:00 2001 From: Adit Sheth Date: Wed, 28 May 2025 12:58:17 -0700 Subject: [PATCH 09/13] Fix invalid comment example in Snippet24 by replacing it with a valid expression statement (#46378) * Fixed bug 46309. * Initialize x. --------- Co-authored-by: Adit Sheth --- .../csProgGuideStatements/CS/Statements.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/samples/snippets/csharp/VS_Snippets_VBCSharp/csProgGuideStatements/CS/Statements.cs b/samples/snippets/csharp/VS_Snippets_VBCSharp/csProgGuideStatements/CS/Statements.cs index 345b8cb1da818..ef9652228ec6c 100644 --- a/samples/snippets/csharp/VS_Snippets_VBCSharp/csProgGuideStatements/CS/Statements.cs +++ b/samples/snippets/csharp/VS_Snippets_VBCSharp/csProgGuideStatements/CS/Statements.cs @@ -535,8 +535,9 @@ void TestMethod(string s) // Expression statement (assignment). area = 3.14 * (radius * radius); - // Error. Not statement because no assignment: - //circ * 2; + // Expression statement (result discarded). + int x = 0; + x++; // Expression statement (method invocation). System.Console.WriteLine(); From 2b54f88be08a826df45571d7b41d6eb6fd9b80b0 Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Wed, 28 May 2025 13:56:57 -0700 Subject: [PATCH 10/13] Delete test policy (#46466) --- .github/policies/disallow-edits.yml | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/.github/policies/disallow-edits.yml b/.github/policies/disallow-edits.yml index cf834f344d3a4..1ead3e81e1331 100644 --- a/.github/policies/disallow-edits.yml +++ b/.github/policies/disallow-edits.yml @@ -60,30 +60,3 @@ configuration: permission: Read then: - closePullRequest - - - description: (This is only a test policy that will be deleted.) Disallow sign-off for articles in the /docs/csharp folder. - if: - - or: - - payloadType: Issue_Comment - - payloadType: Pull_Request_Review_Comment - - isAction: - action: Created - - isActivitySender: - issueAuthor: True - - commentContains: - pattern: '#sign-off' - isRegex: False - - isAssignedToUser: - user: BillWagner - - not: - isActivitySender: - user: BillWagner - then: - # Add the do-not-merge label, remove the ready-to-merge label, and add a reply asking the PR author not to sign off on the PR. - - addReply: - reply: >- - @${issueAuthor} - Please do NOT sign off on this pull request!! - - addLabel: - label: do-not-merge - - removeLabel: - label: ready-to-merge From 41e0a166f90773d1472649d4b6508d2489dda8e3 Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Wed, 28 May 2025 14:00:26 -0700 Subject: [PATCH 11/13] add note about package output (#46463) --- docs/core/sdk/artifacts-output.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/core/sdk/artifacts-output.md b/docs/core/sdk/artifacts-output.md index 988508974857a..69e3ab1d2d32b 100644 --- a/docs/core/sdk/artifacts-output.md +++ b/docs/core/sdk/artifacts-output.md @@ -1,7 +1,7 @@ --- title: Artifacts output layout description: Learn about a .NET SDK feature that centralizes and simplifies the layout of project outputs. -ms.date: 10/31/2023 +ms.date: 05/28/2025 --- # Artifacts output layout @@ -18,23 +18,25 @@ By default, the common location is a directory named *artifacts* next to the *Di The following table shows the default values for each level in the folder structure. You can override the values, as well as the default location, using properties in the *Directory.build.props* file. -| Folder level | Description | Examples | -|--|--|--| -| Type of output | Categories of build outputs, such as binaries, intermediate/generated files, published applications, and NuGet packages. |`bin`, `obj`, `publish`, `package` | -| Project name | Separates output by each project. | `MyApp` | -| Pivot | Distinguishes between builds of a project for different configurations, target frameworks, and runtime identifiers. If multiple elements are needed, they're joined by an underscore (`_`). Can be customized using the `ArtifactsPivots` MSBuild property. | `debug`, `debug_net8.0`, `release`, `release_linux-x64` | +| Folder level | Description | Examples | +|----------------|-----------------------------------|------------------------------------| +| Type of output | Categories of build outputs, such as binaries, intermediate/generated files, published applications, and NuGet packages. | `bin`, `obj`, `publish`, `package` | +| Project name† | Separates output by each project. | `MyApp` | +| Pivot† | Distinguishes between builds of a project for different configurations, target frameworks, and runtime identifiers. If multiple elements are needed, they're joined by an underscore (`_`). Can be [customized](#how-to-configure) using the `ArtifactsPivots` MSBuild property. | `debug`, `debug_net8.0`, `release`, `release_linux-x64` | + +† The project name subfolder is omitted for package output paths. In addition, the pivot subfolder includes only the configuration. ## Examples The following table shows examples of paths that might be created. -| Path | Description | -|---------------------------------------------|--------------------------------------------------------------------------------| -| *artifacts\bin\MyApp\debug* | The build output path for a simple project when you run `dotnet build`. | -| *artifacts\obj\MyApp\debug* | The intermediate output path for a simple project when you run `dotnet build`. | -| *artifacts\bin\MyApp\debug_net8.0* | The build output path for the `net8.0` build of a multi-targeted project. | -| *artifacts\publish\MyApp\release_linux-x64* | The publish path for a simple app when publishing for `linux-x64`. | -| *artifacts\package\MyApp\release* | The folder where the release *.nupkg* is created for a project. | +| Path | Description | +|------------------------------------|--------------------------------------------------------------------------------| +| *artifacts\bin\MyApp\debug* | The build output path for a simple project when you run `dotnet build`. | +| *artifacts\obj\MyApp\debug* | The intermediate output path for a simple project when you run `dotnet build`. | +| *artifacts\bin\MyApp\debug_net8.0* | The build output path for the `net8.0` build of a multi-targeted project. | +| *artifacts\publish\MyApp\release_linux-x64* | The publish path for a simple app when publishing for `linux-x64`. | +| *artifacts\package\release* | The folder where the release *.nupkg* is created for a project. | ## How to configure From 2d452cfe97eb117a3cf24732e582cdde5e5ac611 Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Wed, 28 May 2025 14:07:54 -0700 Subject: [PATCH 12/13] Note that HandleProcessCorruptedStateExceptions attribute is obsolete (#46464) --- .../system-accessviolationexception.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/fundamentals/runtime-libraries/system-accessviolationexception.md b/docs/fundamentals/runtime-libraries/system-accessviolationexception.md index 89b0f1b49681a..3883e2df177c9 100644 --- a/docs/fundamentals/runtime-libraries/system-accessviolationexception.md +++ b/docs/fundamentals/runtime-libraries/system-accessviolationexception.md @@ -1,7 +1,7 @@ --- title: System.AccessViolationException class description: Learn about the System.AccessViolationException class. -ms.date: 12/31/2023 +ms.date: 05/28/2025 --- # System.AccessViolationException class @@ -9,7 +9,7 @@ ms.date: 12/31/2023 An access violation occurs in unmanaged or unsafe code when the code attempts to read or write to memory that has not been allocated, or to which it does not have access. This usually occurs because a pointer has a bad value. Not all reads or writes through bad pointers lead to access violations, so an access violation usually indicates that several reads or writes have occurred through bad pointers, and that memory might be corrupted. Thus, access violations almost always indicate serious programming errors. An clearly identifies these serious errors. -In programs consisting entirely of verifiable managed code, all references are either valid or null, and access violations are impossible. Any operation that attempts to reference a null reference in verifiable code throws a exception. An occurs only when verifiable managed code interacts with unmanaged code or with unsafe managed code. +In programs that consist entirely of verifiable managed code, all references are either valid or null, and access violations are impossible. Any operation that attempts to reference a null reference in verifiable code throws a exception. An occurs only when verifiable managed code interacts with unmanaged code or with unsafe managed code. ## Troubleshoot AccessViolationException exceptions @@ -22,10 +22,15 @@ In either case, you can identify and correct the cause of the exception is always thrown by an attempt to access protected memory—that is, to access memory that's not allocated or that's not owned by a process. - Automatic memory management is one of the services that the .NET runtime provides. If managed code provides the same functionality as your unmanaged code, you may wish to move to managed code to take advantage of this functionality. For more information, see [Automatic Memory Management](../../standard/automatic-memory-management.md). + Automatic memory management is one of the services that the .NET runtime provides. If managed code provides the same functionality as your unmanaged code, consider moving to managed code to take advantage of this functionality. For more information, see [Automatic memory management](../../standard/automatic-memory-management.md). -- Make sure that the memory that you're attempting to access has not been corrupted. If several read or write operations have occurred through bad pointers, memory might be corrupted. This typically occurs when reading or writing to addresses outside of a predefined buffer. +- Make sure that the memory that you're attempting to access hasn't been corrupted. If several read or write operations have occurred through bad pointers, memory might be corrupted. This typically occurs when reading or writing to addresses outside of a predefined buffer. ## AccessViolationException and try/catch blocks - exceptions thrown by the .NET runtime aren't handled by the `catch` statement in a structured exception handler if the exception occurs outside of the memory reserved by the runtime. To handle such an exception, apply the attribute to the method in which the exception is thrown. This change does not affect exceptions thrown by user code, which can continue to be caught by a `catch` statement. + exceptions thrown by the .NET runtime aren't handled by the `catch` statement in a structured exception handler if the exception occurs outside of the memory reserved by the runtime. + +**.NET Framework only**: To handle such an exception, apply the attribute to the method in which the exception is thrown. This change does not affect exceptions thrown by user code, which can continue to be caught by a `catch` statement. + +> [!CAUTION] +> The [HandleProcessCorruptedStateExceptions attribute](xref:System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptionsAttribute) is obsolete in current .NET versions. Recovery from corrupted process state–exceptions isn't supported, and the attribute, if present, is ignored. From 20e230aa69dac084f09c2417444f54afc7fbf0e1 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 28 May 2025 16:04:53 -0700 Subject: [PATCH 13/13] Fix entrypoint name for MessageBox sample (#46462) --- samples/snippets/standard/interop/pinvoke/messagebox.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/snippets/standard/interop/pinvoke/messagebox.cs b/samples/snippets/standard/interop/pinvoke/messagebox.cs index a3d4a978ee3ee..ee4cf76d0c0fc 100644 --- a/samples/snippets/standard/interop/pinvoke/messagebox.cs +++ b/samples/snippets/standard/interop/pinvoke/messagebox.cs @@ -6,11 +6,11 @@ public partial class Program // Import user32.dll (containing the function we need) and define // the method corresponding to the native function. [LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)] - private static partial int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType); + private static partial int MessageBoxW(IntPtr hWnd, string lpText, string lpCaption, uint uType); public static void Main(string[] args) { // Invoke the function as a regular managed method. - MessageBox(IntPtr.Zero, "Command-line message box", "Attention!", 0); + MessageBoxW(IntPtr.Zero, "Command-line message box", "Attention!", 0); } }