diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml index 4dcdfb951..0c96fa9ca 100644 --- a/.github/workflows/itests.yml +++ b/.github/workflows/itests.yml @@ -48,8 +48,8 @@ jobs: GOOS: linux GOARCH: amd64 GOPROXY: https://proxy.golang.org - DAPR_CLI_VER: 1.14.0 - DAPR_RUNTIME_VER: 1.14.0 + DAPR_CLI_VER: 1.15.0 + DAPR_RUNTIME_VER: 1.15.3 DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/release-1.14/install/install.sh DAPR_CLI_REF: '' steps: diff --git a/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md b/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md index 09422e474..a5d60c2fa 100644 --- a/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md +++ b/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md @@ -20,7 +20,7 @@ To load secrets into configuration call the _AddDaprSecretStore_ extension metho Use Dapr to run the application: ```shell -dapr run --app-id SecretStoreConfigurationProviderSample --components-path ./components/ -- dotnet run +dapr run --app-id SecretStoreConfigurationProviderSample --resources-path ./components/ -- dotnet run ``` ### 2. Test the application diff --git a/examples/Client/ConfigurationApi/README.md b/examples/Client/ConfigurationApi/README.md index 7425a780a..d73a29f9f 100644 --- a/examples/Client/ConfigurationApi/README.md +++ b/examples/Client/ConfigurationApi/README.md @@ -147,7 +147,7 @@ cd examples/Client/ConfigurationApi To run the `ConfigurationExample`, execute the following command: ```bash -dapr run --app-id configexample --components-path ./Components -- dotnet run +dapr run --app-id configexample --resources-path ./Components -- dotnet run ``` ### Get Configuration diff --git a/examples/Client/DistributedLock/README.md b/examples/Client/DistributedLock/README.md index cdac6f91a..6a1af3b34 100644 --- a/examples/Client/DistributedLock/README.md +++ b/examples/Client/DistributedLock/README.md @@ -24,7 +24,7 @@ cd examples/Client/DistributedLock In order to run the application that generates data for the workers to process, simply run the following command: ```bash -dapr run --components-path ./Components --app-id generator -- dotnet run +dapr run --resources-path ./Components --app-id generator -- dotnet run ``` This application will create a new file to process once every 10 seconds. The files are stored in `DistributedLock/tmp`. @@ -33,8 +33,8 @@ This application will create a new file to process once every 10 seconds. The fi In order to properly demonstrate locking, this application will be run more than once with the same App ID. However, the applications do need different ports in order to properly receive bindings. Run them with the command below: ```bash -dapr run --components-path ./Components --app-id worker --app-port 5000 -- dotnet run -dapr run --components-path ./Components --app-id worker --app-port 5001 -- dotnet run +dapr run --resources-path ./Components --app-id worker --app-port 5000 -- dotnet run +dapr run --resources-path ./Components --app-id worker --app-port 5001 -- dotnet run ``` After running the applications, they will attempt to process files. You should see output such as: diff --git a/src/Dapr.Actors/IDaprInteractor.cs b/src/Dapr.Actors/IDaprInteractor.cs index e0d91c44f..dde86b918 100644 --- a/src/Dapr.Actors/IDaprInteractor.cs +++ b/src/Dapr.Actors/IDaprInteractor.cs @@ -11,110 +11,108 @@ // limitations under the License. // ------------------------------------------------------------------------ +namespace Dapr.Actors; + using System.Net.Http; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors.Communication; -namespace Dapr.Actors +/// +/// Interface for interacting with Dapr runtime. +/// +internal interface IDaprInteractor { - using System.IO; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Actors.Communication; - /// - /// Interface for interacting with Dapr runtime. + /// Invokes an Actor method on Dapr without remoting. /// - internal interface IDaprInteractor - { - /// - /// Invokes an Actor method on Dapr without remoting. - /// - /// Type of actor. - /// ActorId. - /// Method name to invoke. - /// Serialized body. - /// Cancels the operation. - /// A task that represents the asynchronous operation. - Task InvokeActorMethodWithoutRemotingAsync(string actorType, string actorId, string methodName, string jsonPayload, CancellationToken cancellationToken = default); + /// Type of actor. + /// ActorId. + /// Method name to invoke. + /// Serialized body. + /// Cancels the operation. + /// A task that represents the asynchronous operation. + Task InvokeActorMethodWithoutRemotingAsync(string actorType, string actorId, string methodName, string jsonPayload, CancellationToken cancellationToken = default); - /// - /// Saves state batch to Dapr. - /// - /// Type of actor. - /// ActorId. - /// JSON data with state changes as per the Dapr spec for transaction state update. - /// Cancels the operation. - /// A task that represents the asynchronous operation. - Task SaveStateTransactionallyAsync(string actorType, string actorId, string data, CancellationToken cancellationToken = default); + /// + /// Saves state batch to Dapr. + /// + /// Type of actor. + /// ActorId. + /// JSON data with state changes as per the Dapr spec for transaction state update. + /// Cancels the operation. + /// A task that represents the asynchronous operation. + Task SaveStateTransactionallyAsync(string actorType, string actorId, string data, CancellationToken cancellationToken = default); - /// - /// Saves a state to Dapr. - /// - /// Type of actor. - /// ActorId. - /// Name of key to get value for. - /// Cancels the operation. - /// A task that represents the asynchronous operation. - Task> GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default); + /// + /// Gets a state from Dapr. + /// + /// Type of actor. + /// ActorId. + /// Name of key to get value for. + /// Cancels the operation. + /// A task that represents the asynchronous operation. + Task> GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default); - /// - /// Invokes Actor method. - /// - /// Serializers manager for remoting calls. - /// Actor Request Message. - /// Cancels the operation. - /// A representing the result of the asynchronous operation. - Task InvokeActorMethodWithRemotingAsync(ActorMessageSerializersManager serializersManager, IActorRequestMessage remotingRequestRequestMessage, CancellationToken cancellationToken = default); + /// + /// Invokes Actor method. + /// + /// Serializers manager for remoting calls. + /// Actor Request Message. + /// Cancels the operation. + /// A representing the result of the asynchronous operation. + Task InvokeActorMethodWithRemotingAsync(ActorMessageSerializersManager serializersManager, IActorRequestMessage remotingRequestRequestMessage, CancellationToken cancellationToken = default); - /// - /// Register a reminder. - /// - /// Type of actor. - /// ActorId. - /// Name of reminder to register. - /// JSON reminder data as per the Dapr spec. - /// Cancels the operation. - /// A representing the result of the asynchronous operation. - Task RegisterReminderAsync(string actorType, string actorId, string reminderName, string data, CancellationToken cancellationToken = default); + /// + /// Register a reminder. + /// + /// Type of actor. + /// ActorId. + /// Name of reminder to register. + /// JSON reminder data as per the Dapr spec. + /// Cancels the operation. + /// A representing the result of the asynchronous operation. + Task RegisterReminderAsync(string actorType, string actorId, string reminderName, string data, CancellationToken cancellationToken = default); - /// - /// Gets a reminder. - /// - /// Type of actor. - /// ActorId. - /// Name of reminder to unregister. - /// Cancels the operation. - /// A containing the response of the asynchronous HTTP operation. - Task GetReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default); + /// + /// Gets a reminder. + /// + /// Type of actor. + /// ActorId. + /// Name of reminder to unregister. + /// Cancels the operation. + /// A containing the response of the asynchronous HTTP operation. + Task GetReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default); - /// - /// Unregisters a reminder. - /// - /// Type of actor. - /// ActorId. - /// Name of reminder to unregister. - /// Cancels the operation. - /// A representing the result of the asynchronous operation. - Task UnregisterReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default); + /// + /// Unregisters a reminder. + /// + /// Type of actor. + /// ActorId. + /// Name of reminder to unregister. + /// Cancels the operation. + /// A representing the result of the asynchronous operation. + Task UnregisterReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default); - /// - /// Registers a timer. - /// - /// Type of actor. - /// ActorId. - /// Name of timer to register. - /// JSON reminder data as per the Dapr spec. - /// Cancels the operation. - /// A representing the result of the asynchronous operation. - Task RegisterTimerAsync(string actorType, string actorId, string timerName, string data, CancellationToken cancellationToken = default); + /// + /// Registers a timer. + /// + /// Type of actor. + /// ActorId. + /// Name of timer to register. + /// JSON reminder data as per the Dapr spec. + /// Cancels the operation. + /// A representing the result of the asynchronous operation. + Task RegisterTimerAsync(string actorType, string actorId, string timerName, string data, CancellationToken cancellationToken = default); - /// - /// Unegisters a timer. - /// - /// Type of actor. - /// ActorId. - /// Name of timer to register. - /// Cancels the operation. - /// A representing the result of the asynchronous operation. - Task UnregisterTimerAsync(string actorType, string actorId, string timerName, CancellationToken cancellationToken = default); - } + /// + /// Unegisters a timer. + /// + /// Type of actor. + /// ActorId. + /// Name of timer to register. + /// Cancels the operation. + /// A representing the result of the asynchronous operation. + Task UnregisterTimerAsync(string actorType, string actorId, string timerName, CancellationToken cancellationToken = default); } diff --git a/src/Dapr.Actors/Runtime/ActorStateCache.cs b/src/Dapr.Actors/Runtime/ActorStateCache.cs new file mode 100644 index 000000000..666cb6625 --- /dev/null +++ b/src/Dapr.Actors/Runtime/ActorStateCache.cs @@ -0,0 +1,224 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +#nullable enable +using System; +using System.Collections.Generic; + +namespace Dapr.Actors.Runtime; + +internal sealed class ActorStateCache : IActorStateCache +{ + /// + /// Maintains the cache state. + /// + private readonly Dictionary stateMetadata = new(); + + /// + /// Adds the indicated value to the cache. + /// + /// The name of the state. + /// The cached value. + /// How far out the TTL expiry should be. + /// The type of value getting cached. + /// stateContainsKey indicates if the cache already contains the key or not and + /// addedToState indicates if the value was added or updated in the cache. + public (bool stateContainsKey, bool addedToState) Add(string stateName, T value, TimeSpan? ttl = null) + { + if (!stateMetadata.TryGetValue(stateName, out var state)) + { + stateMetadata.Add(stateName, StateMetadata.Create(value, StateChangeKind.Add, ttl)); + return (false, true); + } + + if (!IsMarkedAsRemoveOrExpired(state)) + { + return (true, false); + } + + stateMetadata[stateName] = StateMetadata.Create(value, StateChangeKind.Update, ttl); + return (true, true); + } + + /// + /// Adds the indicated value to the cache. + /// + /// The name of the state. + /// The cached value. + /// The TTL expiry timestamp. + /// The type of value getting cached. + /// stateContainsKey indicates if the cache already contains the key or not and + /// addedToState indicates if the value was added or updated in the cache. + public (bool stateContainsKey, bool addedToState) Add(string stateName, T value, DateTimeOffset ttlExpiry) + { + if (!stateMetadata.TryGetValue(stateName, out var state)) + { + stateMetadata.Add(stateName, StateMetadata.Create(value, StateChangeKind.Add, ttlExpiry)); + return (false, true); + } + + if (!IsMarkedAsRemoveOrExpired(state)) + { + return (true, false); + } + + stateMetadata[stateName] = StateMetadata.Create(value, StateChangeKind.Update, ttlExpiry); + return (true, true); + + } + + /// + /// Sets the cache with the specified value whether it already exists or not. + /// + /// The name of the state to save the value to. + /// The state metadata to save to the cache. + public void Set(string stateName, StateMetadata metadata) + { + stateMetadata[stateName] = metadata; + } + + /// + /// Removes the indicated state name from the cache. + /// + /// The name of the state to remove. + public void Remove(string stateName) => stateMetadata.Remove(stateName); + + /// + /// Retrieves the current state from the cache if available and not expired. + /// + /// The name of the state to retrieve. + /// If available and not expired, the value of the state persisted in the cache. + /// True if the cache contains the state name; false if not. + public (bool containsKey, bool isMarkedAsRemoveOrExpired) TryGet(string stateName, out StateMetadata? metadata) + { + var isMarkedAsRemoveOrExpired = false; + metadata = null; + + if (!stateMetadata.TryGetValue(stateName, out var state)) + { + return (false, false); + } + + if (IsMarkedAsRemoveOrExpired(state)) + { + isMarkedAsRemoveOrExpired = true; + } + + metadata = state; + return (true, isMarkedAsRemoveOrExpired); + + } + + /// + /// Clears the all the data from the cache. + /// + public void Clear() + { + stateMetadata.Clear(); + } + + /// + /// Builds out the change lists of states to update in the provider and states to remove from the cache. This + /// is typically only called by invocation of the SaveStateAsync method in . + /// + /// The list of state changes and states to remove from the cache. + public (IReadOnlyList stateChanges, IReadOnlyList statesToRemove) BuildChangeList() + { + var stateChanges = new List(); + var statesToRemove = new List(); + + if (stateMetadata.Count == 0) + { + return (stateChanges, statesToRemove); + } + + foreach (var stateName in stateMetadata.Keys) + { + var metadata = stateMetadata[stateName]; + if (metadata.ChangeKind is not StateChangeKind.None) + { + stateChanges.Add(new ActorStateChange(stateName, metadata.Type, metadata.Value, metadata.ChangeKind, metadata.TTLExpireTime)); + + if (metadata.ChangeKind is StateChangeKind.Remove) + { + statesToRemove.Add(stateName); + } + + //Mark the states as unmodified so the tracking for the next invocation is done correctly + var updatedState = metadata with { ChangeKind = StateChangeKind.None }; + stateMetadata[stateName] = updatedState; + } + } + + return (stateChanges, statesToRemove); + } + + /// + /// Helper method that determines if a state metadata is expired. + /// + /// The metadata to evaluate. + /// True if the state metadata is marked for removal or the TTL has expired, otherwise false. + public bool IsMarkedAsRemoveOrExpired(StateMetadata metadata) => + metadata.ChangeKind == StateChangeKind.Remove || (metadata.TTLExpireTime.HasValue && + metadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow); + + /// + /// Exposed for testing only. + /// + /// + internal Dictionary GetStateMetadata() => stateMetadata; + + internal sealed record StateMetadata + { + /// + /// This should only be used for testing purposes. Use the static `Create` methods for any actual usage. + /// + /// + /// + /// + /// + /// + /// + internal StateMetadata(object? value, Type type, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime = null, TimeSpan? ttl = null) + { + this.Value = value; + this.Type = type; + this.ChangeKind = changeKind; + + if (ttlExpireTime.HasValue && ttl.HasValue) { + throw new ArgumentException("Cannot specify both TTLExpireTime and TTL"); + } + + this.TTLExpireTime = ttl.HasValue ? DateTimeOffset.UtcNow.Add(ttl.Value) : ttlExpireTime; + } + + public object? Value { get; init; } + + public StateChangeKind ChangeKind { get; init; } + + public Type Type { get; init; } + + public DateTimeOffset? TTLExpireTime { get; init; } + + public static StateMetadata Create(T? value, StateChangeKind changeKind) => + new(value, typeof(T), changeKind); + + public static StateMetadata Create(T? value, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime) => + new(value, typeof(T), changeKind, ttlExpireTime: ttlExpireTime); + + public static StateMetadata Create(T? value, StateChangeKind changeKind, TimeSpan? ttl) => + new(value, typeof(T), changeKind, ttl: ttl); + + public static StateMetadata CreateForRemove() => new(null, typeof(object), StateChangeKind.Remove); + } +} diff --git a/src/Dapr.Actors/Runtime/ActorStateChange.cs b/src/Dapr.Actors/Runtime/ActorStateChange.cs index 34fa68fdf..f90338f6d 100644 --- a/src/Dapr.Actors/Runtime/ActorStateChange.cs +++ b/src/Dapr.Actors/Runtime/ActorStateChange.cs @@ -1,5 +1,5 @@ // ------------------------------------------------------------------------ -// Copyright 2021 The Dapr Authors +// Copyright 2025 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -11,75 +11,22 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime -{ - using System; - - /// - /// Represents a change to an actor state with a given state name. - /// - public sealed class ActorStateChange - { - /// - /// Initializes a new instance of the class. - /// - /// The name of the actor state. - /// The type of value associated with given actor state name. - /// The value associated with given actor state name. - /// The kind of state change for given actor state name. - /// The time to live for the state. - public ActorStateChange(string stateName, Type type, object value, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime) - { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - - this.StateName = stateName; - this.Type = type; - this.Value = value; - this.ChangeKind = changeKind; - this.TTLExpireTime = ttlExpireTime; - } - - /// - /// Gets the name of the actor state. - /// - /// - /// The name of the actor state. - /// - public string StateName { get; } - - /// - /// Gets the type of value associated with given actor state name. - /// - /// - /// The type of value associated with given actor state name. - /// - public Type Type { get; } - - /// - /// Gets the value associated with given actor state name. - /// - /// - /// The value associated with given actor state name. - /// - public object Value { get; } - - /// - /// Gets the kind of state change for given actor state name. - /// - /// - /// The kind of state change for given actor state name. - /// - public StateChangeKind ChangeKind { get; } - - /// - /// Gets the time to live for the state. - /// - /// - /// The time to live for the state. - /// - /// - /// If null, the state will not expire. - /// - public DateTimeOffset? TTLExpireTime { get; } - } -} +#nullable enable +namespace Dapr.Actors.Runtime; + +using System; + +/// +/// Represents a change to an actor state with a given state name. +/// +/// The name of the actor state. +/// The type of value associated with the given actor state name. +/// The value associated with the given actor state name. +/// The kind of state change for the given actor state name. +/// The time to live for the state. If null, the state wil not expire. +public sealed record ActorStateChange( + string StateName, + Type Type, + object? Value, + StateChangeKind ChangeKind, + DateTimeOffset? TTLExpireTime); diff --git a/src/Dapr.Actors/Runtime/ActorStateManager.cs b/src/Dapr.Actors/Runtime/ActorStateManager.cs index 31ada4433..b496f1589 100644 --- a/src/Dapr.Actors/Runtime/ActorStateManager.cs +++ b/src/Dapr.Actors/Runtime/ActorStateManager.cs @@ -19,567 +19,458 @@ using Dapr.Actors.Resources; using Dapr.Actors.Communication; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +internal sealed class ActorStateManager : IActorStateManager, IActorContextualState { - internal sealed class ActorStateManager : IActorStateManager, IActorContextualState + private readonly Actor actor; + private readonly string actorTypeName; + private readonly IActorStateCache defaultCache; + private static readonly AsyncLocal<(string id, IActorStateCache stateCache)> context = new(); + + internal ActorStateManager(Actor actor) { - private readonly Actor actor; - private readonly string actorTypeName; - private readonly Dictionary defaultTracker; - private static AsyncLocal<(string id, Dictionary tracker)> context = new AsyncLocal<(string, Dictionary)>(); + this.actor = actor; + this.actorTypeName = actor.Host.ActorTypeInfo.ActorTypeName; + this.defaultCache = new ActorStateCache(); + } - internal ActorStateManager(Actor actor) - { - this.actor = actor; - this.actorTypeName = actor.Host.ActorTypeInfo.ActorTypeName; - this.defaultTracker = new Dictionary(); - } + internal ActorStateManager(Actor actor, IActorStateCache stateCache) + { + this.actor = actor; + this.actorTypeName = actor.Host.ActorTypeInfo.ActorTypeName; + this.defaultCache = stateCache; + } - public async Task AddStateAsync(string stateName, T value, CancellationToken cancellationToken) - { - EnsureStateProviderInitialized(); + public async Task AddStateAsync(string stateName, T value, CancellationToken cancellationToken) + { + EnsureStateProviderInitialized(); - if (!(await this.TryAddStateAsync(stateName, value, cancellationToken))) - { - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ActorStateAlreadyExists, stateName)); - } + if (!(await this.TryAddStateAsync(stateName, value, cancellationToken))) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ActorStateAlreadyExists, stateName)); } + } - public async Task AddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken) - { - EnsureStateProviderInitialized(); + public async Task AddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken) + { + EnsureStateProviderInitialized(); - if (!(await this.TryAddStateAsync(stateName, value, ttl, cancellationToken))) - { - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ActorStateAlreadyExists, stateName)); - } + if (!(await this.TryAddStateAsync(stateName, value, ttl, cancellationToken))) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ActorStateAlreadyExists, stateName)); } + } - public async Task TryAddStateAsync(string stateName, T value, CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + public async Task TryAddStateAsync(string stateName, T value, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - EnsureStateProviderInitialized(); + EnsureStateProviderInitialized(); - var stateChangeTracker = GetContextualStateTracker(); + var cache = GetContextualStateTracker(); + var (stateContainsKey, addedToState) = cache.Add(stateName, value); + if (stateContainsKey) + { + return addedToState; + } - if (stateChangeTracker.ContainsKey(stateName)) - { - var stateMetadata = stateChangeTracker[stateName]; + var containsStateResult = await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, + this.actor.Id.ToString(), stateName, cancellationToken); + if (containsStateResult) + { + //Return false because we shouldn't add a value already present in the provider + return false; + } - // Check if the property was marked as remove or is expired in the cache - if (stateMetadata.ChangeKind == StateChangeKind.Remove || (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow)) - { - stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Update); - return true; - } + //Add to the cache + cache.Set(stateName, ActorStateCache.StateMetadata.Create(value, StateChangeKind.Add)); + return addedToState; + } - return false; - } + public async Task TryAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) - { - return false; - } + EnsureStateProviderInitialized(); - stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Add); - return true; + var cache = GetContextualStateTracker(); + var (stateContainsKey, addedToState) = cache.Add(stateName, value, ttl); + if (stateContainsKey) + { + return addedToState; } - - public async Task TryAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default) + + if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - - EnsureStateProviderInitialized(); - - var stateChangeTracker = GetContextualStateTracker(); - - if (stateChangeTracker.ContainsKey(stateName)) - { - var stateMetadata = stateChangeTracker[stateName]; + return false; + } - // Check if the property was marked as remove in the cache or has been expired. - if (stateMetadata.ChangeKind == StateChangeKind.Remove || (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow)) - { - stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Update, ttl: ttl); - return true; - } + //Add to the cache + cache.Set(stateName, ActorStateCache.StateMetadata.Create(value, StateChangeKind.Add, ttl)); + return addedToState; + } - return false; - } + public async Task GetStateAsync(string stateName, CancellationToken cancellationToken) + { + EnsureStateProviderInitialized(); - if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) - { - return false; - } + var condRes = await this.TryGetStateAsync(stateName, cancellationToken); - stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Add, ttl: ttl); - return true; + if (condRes.HasValue) + { + return condRes.Value; } - public async Task GetStateAsync(string stateName, CancellationToken cancellationToken) - { - EnsureStateProviderInitialized(); + throw new KeyNotFoundException(string.Format(CultureInfo.CurrentCulture, SR.ErrorNamedActorStateNotFound, stateName)); + } - var condRes = await this.TryGetStateAsync(stateName, cancellationToken); + public async Task> TryGetStateAsync(string stateName, CancellationToken cancellationToken) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - if (condRes.HasValue) - { - return condRes.Value; - } + EnsureStateProviderInitialized(); - throw new KeyNotFoundException(string.Format(CultureInfo.CurrentCulture, SR.ErrorNamedActorStateNotFound, stateName)); + var stateChangeTracker = GetContextualStateTracker(); + var getCacheValue = stateChangeTracker.TryGet(stateName, out var state); + if (getCacheValue.containsKey) + { + return getCacheValue.isMarkedAsRemoveOrExpired + ? new ConditionalValue(false, default) + : new ConditionalValue(true, (T)state!.Value); } - - public async Task> TryGetStateAsync(string stateName, CancellationToken cancellationToken) + + var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken); + if (conditionalResult.HasValue) { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - - EnsureStateProviderInitialized(); + var stateMetadata = ActorStateCache.StateMetadata.Create(conditionalResult.Value.Value, + StateChangeKind.None, conditionalResult.Value.TTLExpireTime); + stateChangeTracker.Add(stateName, stateMetadata); + return new ConditionalValue(true, conditionalResult.Value.Value); + } - var stateChangeTracker = GetContextualStateTracker(); + return new ConditionalValue(false, default); + } - if (stateChangeTracker.ContainsKey(stateName)) - { - var stateMetadata = stateChangeTracker[stateName]; + public async Task SetStateAsync(string stateName, T value, CancellationToken cancellationToken) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - // Check if the property was marked as remove in the cache or is expired - if (stateMetadata.ChangeKind == StateChangeKind.Remove || (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow)) - { - return new ConditionalValue(false, default); - } + EnsureStateProviderInitialized(); - return new ConditionalValue(true, (T)stateMetadata.Value); - } - - var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken); - if (conditionalResult.HasValue) + var stateChangeTracker = GetContextualStateTracker(); + var (cacheContainsKey, _) = stateChangeTracker.TryGet(stateName, out var state); + if (cacheContainsKey && state is not null) + { + var updatedState = state with { Value = value, TTLExpireTime = null }; + if (state.ChangeKind is StateChangeKind.None or StateChangeKind.Remove) { - stateChangeTracker.Add(stateName, StateMetadata.Create(conditionalResult.Value.Value, StateChangeKind.None, ttlExpireTime: conditionalResult.Value.TTLExpireTime)); - return new ConditionalValue(true, conditionalResult.Value.Value); + updatedState = updatedState with { ChangeKind = StateChangeKind.Update }; } - return new ConditionalValue(false, default); + stateChangeTracker.Set(stateName, updatedState); } - - public async Task SetStateAsync(string stateName, T value, CancellationToken cancellationToken) + else if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), + stateName, cancellationToken)) { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - - EnsureStateProviderInitialized(); - - var stateChangeTracker = GetContextualStateTracker(); - - if (stateChangeTracker.ContainsKey(stateName)) - { - var stateMetadata = stateChangeTracker[stateName]; - stateMetadata.Value = value; - stateMetadata.TTLExpireTime = null; - - if (stateMetadata.ChangeKind == StateChangeKind.None || - stateMetadata.ChangeKind == StateChangeKind.Remove) - { - stateMetadata.ChangeKind = StateChangeKind.Update; - } - } - else if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) - { - stateChangeTracker.Add(stateName, StateMetadata.Create(value, StateChangeKind.Update)); - } - else - { - stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Add); - } + stateChangeTracker.Add(stateName, ActorStateCache.StateMetadata.Create(value, StateChangeKind.Update)); } - - public async Task SetStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken) + else { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + stateChangeTracker.Set(stateName, ActorStateCache.StateMetadata.Create(value, StateChangeKind.Add)); + } + } - EnsureStateProviderInitialized(); + public async Task SetStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - var stateChangeTracker = GetContextualStateTracker(); + EnsureStateProviderInitialized(); - if (stateChangeTracker.ContainsKey(stateName)) - { - var stateMetadata = stateChangeTracker[stateName]; - stateMetadata.Value = value; - stateMetadata.TTLExpireTime = DateTimeOffset.UtcNow.Add(ttl); - - if (stateMetadata.ChangeKind == StateChangeKind.None || - stateMetadata.ChangeKind == StateChangeKind.Remove) - { - stateMetadata.ChangeKind = StateChangeKind.Update; - } - } - else if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) - { - stateChangeTracker.Add(stateName, StateMetadata.Create(value, StateChangeKind.Update, ttl: ttl)); - } - else + var stateChangeTracker = GetContextualStateTracker(); + var getCacheValue = stateChangeTracker.TryGet(stateName, out var state); + if (getCacheValue.containsKey && state is not null) + { + var updatedState = state with { Value = state.Value, TTLExpireTime = DateTimeOffset.UtcNow.Add(ttl) }; + if (updatedState.ChangeKind is StateChangeKind.None or StateChangeKind.Remove) { - stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Add, ttl: ttl); + updatedState = updatedState with { ChangeKind = StateChangeKind.Update }; } + stateChangeTracker.Set(stateName, updatedState); } - - public async Task RemoveStateAsync(string stateName, CancellationToken cancellationToken) + else if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), + stateName, cancellationToken)) { - EnsureStateProviderInitialized(); - - if (!(await this.TryRemoveStateAsync(stateName, cancellationToken))) - { - throw new KeyNotFoundException(string.Format(CultureInfo.CurrentCulture, SR.ErrorNamedActorStateNotFound, stateName)); - } + stateChangeTracker.Add(stateName, ActorStateCache.StateMetadata.Create(value, StateChangeKind.Update, ttl)); } - - public async Task TryRemoveStateAsync(string stateName, CancellationToken cancellationToken) + else { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - - EnsureStateProviderInitialized(); - - var stateChangeTracker = GetContextualStateTracker(); - - if (stateChangeTracker.ContainsKey(stateName)) - { - var stateMetadata = stateChangeTracker[stateName]; - - if (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow) - { - stateChangeTracker.Remove(stateName); - return false; - } - - switch (stateMetadata.ChangeKind) - { - case StateChangeKind.Remove: - return false; - case StateChangeKind.Add: - stateChangeTracker.Remove(stateName); - return true; - } - - stateMetadata.ChangeKind = StateChangeKind.Remove; - return true; - } + stateChangeTracker.Set(stateName, ActorStateCache.StateMetadata.Create(value, StateChangeKind.Add, ttl)); + } + } - if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) - { - stateChangeTracker.Add(stateName, StateMetadata.CreateForRemove()); - return true; - } + public async Task RemoveStateAsync(string stateName, CancellationToken cancellationToken) + { + EnsureStateProviderInitialized(); - return false; + if (!(await this.TryRemoveStateAsync(stateName, cancellationToken))) + { + throw new KeyNotFoundException(string.Format(CultureInfo.CurrentCulture, SR.ErrorNamedActorStateNotFound, stateName)); } + } - public async Task ContainsStateAsync(string stateName, CancellationToken cancellationToken) - { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + public async Task TryRemoveStateAsync(string stateName, CancellationToken cancellationToken) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - EnsureStateProviderInitialized(); + EnsureStateProviderInitialized(); - var stateChangeTracker = GetContextualStateTracker(); + var stateChangeTracker = GetContextualStateTracker(); - if (stateChangeTracker.ContainsKey(stateName)) + var cacheGetResult = stateChangeTracker.TryGet(stateName, out var state); + if (cacheGetResult.containsKey && state is not null) + { + if (cacheGetResult.isMarkedAsRemoveOrExpired) { - var stateMetadata = stateChangeTracker[stateName]; - - // Check if the property was marked as remove in the cache - return stateMetadata.ChangeKind != StateChangeKind.Remove; + stateChangeTracker.Remove(stateName); + return false; } - if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) + switch (state.ChangeKind) { - return true; + case StateChangeKind.Remove: + return false; + case StateChangeKind.Add: + stateChangeTracker.Remove(stateName); + return true; } - return false; + var updatedState = state with { ChangeKind = StateChangeKind.Remove }; + stateChangeTracker.Set(stateName, updatedState); + return true; } - - public async Task GetOrAddStateAsync(string stateName, T value, CancellationToken cancellationToken) + + if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) { - EnsureStateProviderInitialized(); + stateChangeTracker.Add(stateName, ActorStateCache.StateMetadata.CreateForRemove()); + return true; + } - var condRes = await this.TryGetStateAsync(stateName, cancellationToken); + return false; + } - if (condRes.HasValue) - { - return condRes.Value; - } + public async Task ContainsStateAsync(string stateName, CancellationToken cancellationToken) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - var changeKind = this.IsStateMarkedForRemove(stateName) ? StateChangeKind.Update : StateChangeKind.Add; + EnsureStateProviderInitialized(); - var stateChangeTracker = GetContextualStateTracker(); - stateChangeTracker[stateName] = StateMetadata.Create(value, changeKind); - return value; + var stateChangeTracker = GetContextualStateTracker(); + var getCacheValue = stateChangeTracker.TryGet(stateName, out var state); + if (getCacheValue.containsKey && state is not null) + { + //Check if the property was marked as remove in the cache + return state.ChangeKind != StateChangeKind.Remove; } - public async Task GetOrAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken) + if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) { - EnsureStateProviderInitialized(); - - var condRes = await this.TryGetStateAsync(stateName, cancellationToken); + return true; + } - if (condRes.HasValue) - { - return condRes.Value; - } + return false; + } - var changeKind = this.IsStateMarkedForRemove(stateName) ? StateChangeKind.Update : StateChangeKind.Add; + public async Task GetOrAddStateAsync(string stateName, T value, CancellationToken cancellationToken) + { + EnsureStateProviderInitialized(); - var stateChangeTracker = GetContextualStateTracker(); - stateChangeTracker[stateName] = StateMetadata.Create(value, changeKind, ttl: ttl); - return value; - } + var condRes = await this.TryGetStateAsync(stateName, cancellationToken); - public async Task AddOrUpdateStateAsync( - string stateName, - T addValue, - Func updateValueFactory, - CancellationToken cancellationToken = default) + if (condRes.HasValue) { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - - EnsureStateProviderInitialized(); + return condRes.Value; + } - var stateChangeTracker = GetContextualStateTracker(); + var changeKind = this.IsStateMarkedForRemove(stateName) ? StateChangeKind.Update : StateChangeKind.Add; - if (stateChangeTracker.ContainsKey(stateName)) - { - var stateMetadata = stateChangeTracker[stateName]; + var stateChangeTracker = GetContextualStateTracker(); + stateChangeTracker.Set(stateName, ActorStateCache.StateMetadata.Create(value, changeKind)); + return value; + } - // Check if the property was marked as remove in the cache - if (stateMetadata.ChangeKind == StateChangeKind.Remove) - { - stateChangeTracker[stateName] = StateMetadata.Create(addValue, StateChangeKind.Update); - return addValue; - } + public async Task GetOrAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken) + { + EnsureStateProviderInitialized(); - var newValue = updateValueFactory.Invoke(stateName, (T)stateMetadata.Value); - stateMetadata.Value = newValue; + var condRes = await this.TryGetStateAsync(stateName, cancellationToken); - if (stateMetadata.ChangeKind == StateChangeKind.None) - { - stateMetadata.ChangeKind = StateChangeKind.Update; - } + if (condRes.HasValue) + { + return condRes.Value; + } - return newValue; - } + var changeKind = this.IsStateMarkedForRemove(stateName) ? StateChangeKind.Update : StateChangeKind.Add; - var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken); - if (conditionalResult.HasValue) - { - var newValue = updateValueFactory.Invoke(stateName, conditionalResult.Value.Value); - stateChangeTracker.Add(stateName, StateMetadata.Create(newValue, StateChangeKind.Update)); + var stateChangeTracker = GetContextualStateTracker(); + stateChangeTracker.Set(stateName, ActorStateCache.StateMetadata.Create(value, changeKind, ttl)); + return value; + } - return newValue; - } + public async Task AddOrUpdateStateAsync( + string stateName, + T addValue, + Func updateValueFactory, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - stateChangeTracker[stateName] = StateMetadata.Create(addValue, StateChangeKind.Add); - return addValue; - } + EnsureStateProviderInitialized(); - public async Task AddOrUpdateStateAsync( - string stateName, - T addValue, - Func updateValueFactory, - TimeSpan ttl, - CancellationToken cancellationToken = default) + var stateChangeTracker = GetContextualStateTracker(); + var getCacheValue = stateChangeTracker.TryGet(stateName, out var state); + if (getCacheValue.containsKey && state is not null) { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - - EnsureStateProviderInitialized(); - - var stateChangeTracker = GetContextualStateTracker(); - - if (stateChangeTracker.ContainsKey(stateName)) + //Check if the property was marked as remove in the cache + if (state.ChangeKind == StateChangeKind.Remove) { - var stateMetadata = stateChangeTracker[stateName]; - - // Check if the property was marked as remove in the cache - if (stateMetadata.ChangeKind == StateChangeKind.Remove) - { - stateChangeTracker[stateName] = StateMetadata.Create(addValue, StateChangeKind.Update, ttl: ttl); - return addValue; - } - - var newValue = updateValueFactory.Invoke(stateName, (T)stateMetadata.Value); - stateMetadata.Value = newValue; - - if (stateMetadata.ChangeKind == StateChangeKind.None) - { - stateMetadata.ChangeKind = StateChangeKind.Update; - } - - return newValue; + stateChangeTracker.Set(stateName, ActorStateCache.StateMetadata.Create(addValue, StateChangeKind.Update)); + return addValue; } - var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken); - if (conditionalResult.HasValue) - { - var newValue = updateValueFactory.Invoke(stateName, conditionalResult.Value.Value); - stateChangeTracker.Add(stateName, StateMetadata.Create(newValue, StateChangeKind.Update, ttl: ttl)); + var newValue = updateValueFactory.Invoke(stateName, (T)state.Value); + var updatedState = state with { Value = newValue }; - return newValue; + if (state.ChangeKind == StateChangeKind.None) + { + updatedState = updatedState with { ChangeKind = StateChangeKind.Update }; } - - stateChangeTracker[stateName] = StateMetadata.Create(addValue, StateChangeKind.Add, ttl: ttl); - return addValue; + + stateChangeTracker.Set(stateName, updatedState); + return newValue; } - public Task ClearCacheAsync(CancellationToken cancellationToken) + var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken); + if (conditionalResult.HasValue) { - EnsureStateProviderInitialized(); + var newValue = updateValueFactory.Invoke(stateName, conditionalResult.Value.Value); + stateChangeTracker.Add(stateName, ActorStateCache.StateMetadata.Create(newValue, StateChangeKind.Update)); - var stateChangeTracker = GetContextualStateTracker(); - - stateChangeTracker.Clear(); - return Task.CompletedTask; + return newValue; } - public async Task SaveStateAsync(CancellationToken cancellationToken = default) - { - EnsureStateProviderInitialized(); + stateChangeTracker.Set(stateName, ActorStateCache.StateMetadata.Create(addValue, StateChangeKind.Add)); + return addValue; + } - var stateChangeTracker = GetContextualStateTracker(); + public async Task AddOrUpdateStateAsync( + string stateName, + T addValue, + Func updateValueFactory, + TimeSpan ttl, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - if (stateChangeTracker.Count > 0) - { - var stateChangeList = new List(); - var statesToRemove = new List(); - - foreach (var stateName in stateChangeTracker.Keys) - { - var stateMetadata = stateChangeTracker[stateName]; - - if (stateMetadata.ChangeKind != StateChangeKind.None) - { - stateChangeList.Add( - new ActorStateChange(stateName, stateMetadata.Type, stateMetadata.Value, stateMetadata.ChangeKind, stateMetadata.TTLExpireTime)); - - if (stateMetadata.ChangeKind == StateChangeKind.Remove) - { - statesToRemove.Add(stateName); - } - - // Mark the states as unmodified so that tracking for next invocation is done correctly. - stateMetadata.ChangeKind = StateChangeKind.None; - } - } - - if (stateChangeList.Count > 0) - { - await this.actor.Host.StateProvider.SaveStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateChangeList.AsReadOnly(), cancellationToken); - } - - // Remove the states from tracker whcih were marked for removal. - foreach (var stateToRemove in statesToRemove) - { - stateChangeTracker.Remove(stateToRemove); - } - } - } + EnsureStateProviderInitialized(); - public Task SetStateContext(string stateContext) + var stateChangeTracker = GetContextualStateTracker(); + var getCacheValue = stateChangeTracker.TryGet(stateName, out var state); + if (getCacheValue.containsKey && state is not null) { - if (stateContext != null) + if (state.ChangeKind == StateChangeKind.Remove) { - context.Value = (stateContext, new Dictionary()); - } - else - { - context.Value = (null, null); + stateChangeTracker.Set(stateName, ActorStateCache.StateMetadata.Create(addValue, StateChangeKind.Update, ttl)); + return addValue; } - return Task.CompletedTask; - } + var newValue = updateValueFactory.Invoke(stateName, (T)state.Value); + var updatedState = state with { Value = newValue }; - private bool IsStateMarkedForRemove(string stateName) - { - var stateChangeTracker = GetContextualStateTracker(); - - if (stateChangeTracker.ContainsKey(stateName) && - stateChangeTracker[stateName].ChangeKind == StateChangeKind.Remove) + if (state.ChangeKind == StateChangeKind.None) { - return true; + updatedState = updatedState with { ChangeKind = StateChangeKind.Update }; } + + stateChangeTracker.Set(stateName, updatedState); - return false; + return newValue; } - private Task>> TryGetStateFromStateProviderAsync(string stateName, CancellationToken cancellationToken) + var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken); + if (conditionalResult.HasValue) { - EnsureStateProviderInitialized(); - return this.actor.Host.StateProvider.TryLoadStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken); - } + var newValue = updateValueFactory.Invoke(stateName, conditionalResult.Value.Value); + stateChangeTracker.Add(stateName, ActorStateCache.StateMetadata.Create(newValue, StateChangeKind.Update, ttl)); - private void EnsureStateProviderInitialized() - { - if (this.actor.Host.StateProvider == null) - { - throw new InvalidOperationException( - "The actor was initialized without a state provider, and so cannot interact with state. " + - "If this is inside a unit test, replace Actor.StateProvider with a mock."); - } + return newValue; } - private Dictionary GetContextualStateTracker() - { - if (context.Value.id != null) - { - return context.Value.tracker; - } - else - { - return defaultTracker; - } - } - - private sealed class StateMetadata - { - private StateMetadata(object value, Type type, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime = null, TimeSpan? ttl = null) - { - this.Value = value; - this.Type = type; - this.ChangeKind = changeKind; - - if (ttlExpireTime.HasValue && ttl.HasValue) { - throw new ArgumentException("Cannot specify both TTLExpireTime and TTL"); - } - if (ttl.HasValue) { - this.TTLExpireTime = DateTimeOffset.UtcNow.Add(ttl.Value); - } else { - this.TTLExpireTime = ttlExpireTime; - } - } + stateChangeTracker.Set(stateName, ActorStateCache.StateMetadata.Create(addValue, StateChangeKind.Add, ttl)); + return addValue; + } - public object Value { get; set; } + public Task ClearCacheAsync(CancellationToken cancellationToken) + { + EnsureStateProviderInitialized(); - public StateChangeKind ChangeKind { get; set; } + var cache = GetContextualStateTracker(); + cache.Clear(); + + return Task.CompletedTask; + } - public Type Type { get; } + public async Task SaveStateAsync(CancellationToken cancellationToken = default) + { + EnsureStateProviderInitialized(); - public DateTimeOffset? TTLExpireTime { get; set; } + var stateChangeTracker = GetContextualStateTracker(); + var (stateChanges, statesToRemove) = stateChangeTracker.BuildChangeList(); - public static StateMetadata Create(T value, StateChangeKind changeKind) + if (stateChanges.Count > 0) + { + await this.actor.Host.StateProvider.SaveStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateChanges, cancellationToken); + } + + //Remove the states from the tracker which were marked for removal + if (statesToRemove.Count > 0) + { + foreach (var stateToRemove in statesToRemove) { - return new StateMetadata(value, typeof(T), changeKind); + stateChangeTracker.Remove(stateToRemove); } + } + } - public static StateMetadata Create(T value, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime) - { - return new StateMetadata(value, typeof(T), changeKind, ttlExpireTime: ttlExpireTime); - } + public Task SetStateContext(string stateContext) + { + context.Value = stateContext != null ? (stateContext, new ActorStateCache()) : (null, null); + return Task.CompletedTask; + } - public static StateMetadata Create(T value, StateChangeKind changeKind, TimeSpan? ttl) - { - return new StateMetadata(value, typeof(T), changeKind, ttl: ttl); - } + private bool IsStateMarkedForRemove(string stateName) + { + var stateChangeTracker = GetContextualStateTracker(); - public static StateMetadata CreateForRemove() - { - return new StateMetadata(null, typeof(object), StateChangeKind.Remove); - } + var getCacheResult = stateChangeTracker.TryGet(stateName, out var state); + return getCacheResult.containsKey && state is not null && state.ChangeKind == StateChangeKind.Remove; + } + + private Task>> TryGetStateFromStateProviderAsync(string stateName, CancellationToken cancellationToken) + { + EnsureStateProviderInitialized(); + return this.actor.Host.StateProvider.TryLoadStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken); + } + + private void EnsureStateProviderInitialized() + { + if (this.actor.Host.StateProvider == null) + { + throw new InvalidOperationException( + "The actor was initialized without a state provider, and so cannot interact with state. " + + "If this is inside a unit test, replace Actor.StateProvider with a mock."); } } + + private IActorStateCache GetContextualStateTracker() => context.Value.id != null ? context.Value.stateCache : defaultCache; } diff --git a/src/Dapr.Actors/Runtime/IActorStateCache.cs b/src/Dapr.Actors/Runtime/IActorStateCache.cs new file mode 100644 index 000000000..10439ae8c --- /dev/null +++ b/src/Dapr.Actors/Runtime/IActorStateCache.cs @@ -0,0 +1,85 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +#nullable enable +using System; +using System.Collections.Generic; + +namespace Dapr.Actors.Runtime; + +internal interface IActorStateCache +{ + /// + /// Adds the indicated value to the cache. + /// + /// The name of the state. + /// The cached value. + /// How far out the TTL expiry should be. + /// The type of value getting cached. + /// stateContainsKey indicates if the cache already contains the key or not and + /// addedToState indicates if the value was added or updated in the cache. + (bool stateContainsKey, bool addedToState) Add(string stateName, T value, TimeSpan? ttl = null); + + /// + /// Adds the indicated value to the cache. + /// + /// The name of the state. + /// The cached value. + /// The TTL expiry timestamp. + /// The type of value getting cached. + /// stateContainsKey indicates if the cache already contains the key or not and + /// addedToState indicates if the value was added or updated in the cache. + (bool stateContainsKey, bool addedToState) Add(string stateName, T value, DateTimeOffset ttlExpiry); + + /// + /// Sets the cache with the specified value whether it already exists or not. + /// + /// The name of the state to save the value to. + /// The state metadata to save to the cache. + void Set(string stateName, ActorStateCache.StateMetadata metadata); + + /// + /// Removes the indicated state name from the cache. + /// + /// The name of the state to remove. + void Remove(string stateName); + + /// + /// Retrieves the current state from the cache if available and not expired. + /// + /// The name of the state to retrieve. + /// If available and not expired, the value of the state persisted in the cache. + /// True if the cache contains the state name; false if not. + (bool containsKey, bool isMarkedAsRemoveOrExpired) TryGet( + string stateName, + out ActorStateCache.StateMetadata? metadata); + + /// + /// Clears the all the data from the cache. + /// + void Clear(); + + /// + /// Builds out the change lists of states to update in the provider and states to remove from the cache. This + /// is typically only called by invocation of the SaveStateAsync method in . + /// + /// + (IReadOnlyList stateChanges, IReadOnlyList statesToRemove) BuildChangeList(); + + /// + /// Helper method that determines if a state metadata is expired. + /// + /// The metadata to evaluate. + /// True if the state metadata is marked for removal or the TTL has expired, otherwise false. + bool IsMarkedAsRemoveOrExpired(ActorStateCache.StateMetadata metadata); +} diff --git a/test/Dapr.Actors.Test/ActorStateManagerTest.cs b/test/Dapr.Actors.Test/ActorStateManagerTest.cs index a4e0e4140..402d7474f 100644 --- a/test/Dapr.Actors.Test/ActorStateManagerTest.cs +++ b/test/Dapr.Actors.Test/ActorStateManagerTest.cs @@ -11,182 +11,203 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Test +namespace Dapr.Actors.Test; + +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using Xunit; +using Dapr.Actors.Communication; +using Dapr.Actors.Runtime; +using Moq; + +/// +/// Contains tests for ActorStateManager. +/// +public sealed class ActorStateManagerTest { - using System; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; - using System.Collections.Generic; - using Xunit; - using Dapr.Actors.Communication; - using Dapr.Actors.Runtime; - using Moq; - - /// - /// Contains tests for ActorStateManager. - /// - public class ActorStateManagerTest + [Fact] + public async Task SetGet() { - [Fact] - public async Task SetGet() - { - var interactor = new Mock(); - var host = ActorHost.CreateForTest(); - host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); - var mngr = new ActorStateManager(new TestActor(host)); - var token = new CancellationToken(); - - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("", null))); - - await mngr.AddStateAsync("key1", "value1", token); - await mngr.AddStateAsync("key2", "value2", token); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - - await Assert.ThrowsAsync(() => mngr.AddStateAsync("key1", "value3", token)); - await Assert.ThrowsAsync(() => mngr.AddStateAsync("key2", "value4", token)); - - await mngr.SetStateAsync("key1", "value5", token); - await mngr.SetStateAsync("key2", "value6", token); - Assert.Equal("value5", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value6", await mngr.GetStateAsync("key2", token)); - } - - [Fact] - public async Task StateWithTTL() - { - var interactor = new Mock(); - var host = ActorHost.CreateForTest(); - host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); - var mngr = new ActorStateManager(new TestActor(host)); - var token = new CancellationToken(); - - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("", null))); - - await mngr.AddStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token); - await mngr.AddStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - - await Task.Delay(TimeSpan.FromSeconds(1.5)); - - await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token)); - await Assert.ThrowsAsync(() => mngr.GetStateAsync("key2", token)); - - // Should be able to add state again after expiry and should not expire. - await mngr.AddStateAsync("key1", "value1", token); - await mngr.AddStateAsync("key2", "value2", token); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - await Task.Delay(TimeSpan.FromSeconds(1.5)); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - } - - [Fact] - public async Task StateRemoveAddTTL() - { - var interactor = new Mock(); - var host = ActorHost.CreateForTest(); - host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); - var mngr = new ActorStateManager(new TestActor(host)); - var token = new CancellationToken(); - - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("", null))); - - await mngr.AddStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token); - await mngr.AddStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - - await mngr.SetStateAsync("key1", "value1", token); - await mngr.SetStateAsync("key2", "value2", token); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - - // TTL is removed so state should not expire. - await Task.Delay(TimeSpan.FromSeconds(1.5)); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - - // Adding TTL back should expire state. - await mngr.SetStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token); - await mngr.SetStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - await Task.Delay(TimeSpan.FromSeconds(1.5)); - await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token)); - await Assert.ThrowsAsync(() => mngr.GetStateAsync("key2", token)); - } - - [Fact] - public async Task StateDaprdExpireTime() - { - var interactor = new Mock(); - var host = ActorHost.CreateForTest(); - host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); - var mngr = new ActorStateManager(new TestActor(host)); - var token = new CancellationToken(); - - // Existing key which has an expiry time. - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("\"value1\"", DateTime.UtcNow.AddSeconds(1)))); - - await Assert.ThrowsAsync(() => mngr.AddStateAsync("key1", "value3", token)); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - - // No longer return the value from the state provider. - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("", null))); - - // Key should be expired after 1 seconds. - await Task.Delay(TimeSpan.FromSeconds(1.5)); - await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token)); - await Assert.ThrowsAsync(() => mngr.RemoveStateAsync("key1", token)); - await mngr.AddStateAsync("key1", "value2", TimeSpan.FromSeconds(1), token); - Assert.Equal("value2", await mngr.GetStateAsync("key1", token)); - } - - [Fact] - public async Task RemoveState() - { - var interactor = new Mock(); - var host = ActorHost.CreateForTest(); - host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); - var mngr = new ActorStateManager(new TestActor(host)); - var token = new CancellationToken(); - - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("", null))); - - await Assert.ThrowsAsync(() => mngr.RemoveStateAsync("key1", token)); - - await mngr.AddStateAsync("key1", "value1", token); - await mngr.AddStateAsync("key2", "value2", token); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - - await mngr.RemoveStateAsync("key1", token); - await mngr.RemoveStateAsync("key2", token); - - await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token)); - await Assert.ThrowsAsync(() => mngr.GetStateAsync("key2", token)); - - // Should be able to add state again after removal. - await mngr.AddStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token); - await mngr.AddStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - } + var interactor = new Mock(); + var host = ActorHost.CreateForTest(); + host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var mngr = new ActorStateManager(new TestActor(host)); + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); + + const string key1 = "key1"; + const string key2 = "key2"; + const string val1 = "value1"; + const string val2 = "value2"; + const string val3 = "value3"; + const string val4 = "value4"; + const string val5 = "value5"; + const string val6 = "value6"; + + await mngr.AddStateAsync(key1, val1, cts.Token); + await mngr.AddStateAsync(key2, val2, cts.Token); + Assert.Equal(val1, await mngr.GetStateAsync(key1, cts.Token)); + Assert.Equal(val2, await mngr.GetStateAsync(key2, cts.Token)); + + await Assert.ThrowsAsync(() => mngr.AddStateAsync(key1, val3, cts.Token)); + await Assert.ThrowsAsync(() => mngr.AddStateAsync(key2, val4, cts.Token)); + + await mngr.SetStateAsync(key1, val5, cts.Token); + await mngr.SetStateAsync(key2, val6, cts.Token); + Assert.Equal(val5, await mngr.GetStateAsync(key1, cts.Token)); + Assert.Equal(val6, await mngr.GetStateAsync(key2, cts.Token)); + } + + [Fact] + public async Task StateWithTTL() + { + var interactor = new Mock(); + var host = ActorHost.CreateForTest(); + host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var mngr = new ActorStateManager(new TestActor(host)); + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); + + const string key1 = "key1"; + const string key2 = "key2"; + const string val1 = "value1"; + const string val2 = "value2"; + + await mngr.AddStateAsync(key1, val1, TimeSpan.FromSeconds(1), cts.Token); + await mngr.AddStateAsync(key2, val2, TimeSpan.FromSeconds(1), cts.Token); + Assert.Equal(val1, await mngr.GetStateAsync(key1, cts.Token)); + Assert.Equal(val2, await mngr.GetStateAsync(key2, cts.Token)); + + await Task.Delay(TimeSpan.FromSeconds(1.5), cts.Token); + + await Assert.ThrowsAsync(() => mngr.GetStateAsync(key1, cts.Token)); + await Assert.ThrowsAsync(() => mngr.GetStateAsync(key2, cts.Token)); + + // Should be able to add state again after expiry and should not expire. + await mngr.AddStateAsync(key1, val1, cts.Token); + await mngr.AddStateAsync(key2, val2, cts.Token); + Assert.Equal(val1, await mngr.GetStateAsync(key1, cts.Token)); + Assert.Equal(val2, await mngr.GetStateAsync(key2, cts.Token)); + await Task.Delay(TimeSpan.FromSeconds(1.5), cts.Token); + Assert.Equal(val1, await mngr.GetStateAsync(key1, cts.Token)); + Assert.Equal(val2, await mngr.GetStateAsync(key2, cts.Token)); + } + + [Fact] + public async Task StateRemoveAddTTL() + { + var interactor = new Mock(); + var host = ActorHost.CreateForTest(); + host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var mngr = new ActorStateManager(new TestActor(host)); + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); + + const string key1 = "key1"; + const string key2 = "key2"; + const string val1 = "value1"; + const string val2 = "value2"; + + await mngr.AddStateAsync(key1, val1, TimeSpan.FromSeconds(1), cts.Token); + await mngr.AddStateAsync(key2, val2, TimeSpan.FromSeconds(1), cts.Token); + Assert.Equal(val1, await mngr.GetStateAsync(key1, cts.Token)); + Assert.Equal(val2, await mngr.GetStateAsync(key2, cts.Token)); + + await mngr.SetStateAsync(key1, val1, cts.Token); + await mngr.SetStateAsync(key2, val2, cts.Token); + Assert.Equal(val1, await mngr.GetStateAsync(key1, cts.Token)); + Assert.Equal(val2, await mngr.GetStateAsync(key2, cts.Token)); + + // TTL is removed so state should not expire. + await Task.Delay(TimeSpan.FromSeconds(1.5), cts.Token); + Assert.Equal(val1, await mngr.GetStateAsync(key1, cts.Token)); + Assert.Equal(val2, await mngr.GetStateAsync(key2, cts.Token)); + + // Adding TTL back should expire state. + await mngr.SetStateAsync(key1, val1, TimeSpan.FromSeconds(1), cts.Token); + await mngr.SetStateAsync(key2, val2, TimeSpan.FromSeconds(1), cts.Token); + Assert.Equal(val1, await mngr.GetStateAsync(key1, cts.Token)); + Assert.Equal(val2, await mngr.GetStateAsync(key2, cts.Token)); + await Task.Delay(TimeSpan.FromSeconds(1.5), cts.Token); + await Assert.ThrowsAsync(() => mngr.GetStateAsync(key1, cts.Token)); + await Assert.ThrowsAsync(() => mngr.GetStateAsync(key2, cts.Token)); + } + + [Fact] + public async Task ValidateStateExpirationAndExceptions() + { + var interactor = new Mock(); + var host = ActorHost.CreateForTest(); + host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var mngr = new ActorStateManager(new TestActor(host)); + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + + // Existing key which has an expiry time of 1 second - this is triggered on the call to `this.actor.Host.StateProvider.ContainsStateAsync` during `mngr.AddStateAsync` + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("\"value1\"", DateTime.UtcNow.AddSeconds(1)))); + + await Assert.ThrowsAsync(() => mngr.AddStateAsync("key1", "value3", TimeSpan.FromSeconds(1), cts.Token)); //This is placed before the interactor runs as cache is checked first + Assert.Equal("value3", await mngr.GetStateAsync("key1", cts.Token)); //Validate against the cache value + + // No longer return the value from the state provider. + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); + + // Key should be expired after 1 seconds. + await Task.Delay(TimeSpan.FromSeconds(1.5), cts.Token); + + // While the key will be in the cache, it should no longer be valid as it expired after a second and we've delayed 1.5 seconds + await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", cts.Token)); + await Assert.ThrowsAsync(() => mngr.RemoveStateAsync("key1", cts.Token)); + await mngr.AddStateAsync("key1", "value2", TimeSpan.FromSeconds(1), cts.Token); + Assert.Equal("value2", await mngr.GetStateAsync("key1", cts.Token)); + } + + [Fact] + public async Task RemoveState() + { + var interactor = new Mock(); + var host = ActorHost.CreateForTest(); + host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var mngr = new ActorStateManager(new TestActor(host)); + var token = new CancellationToken(); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); + + await Assert.ThrowsAsync(() => mngr.RemoveStateAsync("key1", token)); + + await mngr.AddStateAsync("key1", "value1", token); + await mngr.AddStateAsync("key2", "value2", token); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + + await mngr.RemoveStateAsync("key1", token); + await mngr.RemoveStateAsync("key2", token); + + await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token)); + await Assert.ThrowsAsync(() => mngr.GetStateAsync("key2", token)); + + // Should be able to add state again after removal. + await mngr.AddStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token); + await mngr.AddStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); } } diff --git a/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs b/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs index b27e9afe3..e22ff9933 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs @@ -18,242 +18,241 @@ using Microsoft.Extensions.Logging.Abstractions; using Xunit; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +public sealed class ActorManagerTests { - public sealed class ActorManagerTests + private ActorManager CreateActorManager(Type type, ActorActivator activator = null) { - private ActorManager CreateActorManager(Type type, ActorActivator activator = null) - { - var registration = new ActorRegistration(ActorTypeInformation.Get(type, actorTypeName: null)); - var interactor = new DaprHttpInteractor(clientHandler: null, "http://localhost:3500", apiToken: null, requestTimeout: null); - return new ActorManager(registration, activator ?? new DefaultActorActivator(), JsonSerializerDefaults.Web, false, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory, interactor); - } + var registration = new ActorRegistration(ActorTypeInformation.Get(type, actorTypeName: null)); + var interactor = new DaprHttpInteractor(clientHandler: null, "http://localhost:3500", apiToken: null, requestTimeout: null); + return new ActorManager(registration, activator ?? new DefaultActorActivator(), JsonSerializerDefaults.Web, false, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory, interactor); + } - [Fact] - public async Task ActivateActorAsync_CreatesActorAndCallsActivateLifecycleMethod() - { - var manager = CreateActorManager(typeof(TestActor)); + [Fact] + public async Task ActivateActorAsync_CreatesActorAndCallsActivateLifecycleMethod() + { + var manager = CreateActorManager(typeof(TestActor)); - var id = ActorId.CreateRandom(); - await manager.ActivateActorAsync(id); + var id = ActorId.CreateRandom(); + await manager.ActivateActorAsync(id); - Assert.True(manager.TryGetActorAsync(id, out var actor)); - Assert.True(Assert.IsType(actor).IsActivated); - } + Assert.True(manager.TryGetActorAsync(id, out var actor)); + Assert.True(Assert.IsType(actor).IsActivated); + } - [Fact] - public async Task ActivateActorAsync_CanActivateMultipleActors() - { - var manager = CreateActorManager(typeof(TestActor)); + [Fact] + public async Task ActivateActorAsync_CanActivateMultipleActors() + { + var manager = CreateActorManager(typeof(TestActor)); - await manager.ActivateActorAsync(new ActorId("1")); - Assert.True(manager.TryGetActorAsync(new ActorId("1"), out var actor1)); + await manager.ActivateActorAsync(new ActorId("1")); + Assert.True(manager.TryGetActorAsync(new ActorId("1"), out var actor1)); - await manager.ActivateActorAsync(new ActorId("2")); - Assert.True(manager.TryGetActorAsync(new ActorId("2"), out var actor2)); + await manager.ActivateActorAsync(new ActorId("2")); + Assert.True(manager.TryGetActorAsync(new ActorId("2"), out var actor2)); - Assert.NotSame(actor1, actor2); - } + Assert.NotSame(actor1, actor2); + } - [Fact] - public async Task ActivateActorAsync_UsesActivator() - { - var activator = new TestActivator(); + [Fact] + public async Task ActivateActorAsync_UsesActivator() + { + var activator = new TestActivator(); - var manager = CreateActorManager(typeof(TestActor), activator); + var manager = CreateActorManager(typeof(TestActor), activator); - var id = ActorId.CreateRandom(); - await manager.ActivateActorAsync(id); + var id = ActorId.CreateRandom(); + await manager.ActivateActorAsync(id); - Assert.Equal(1, activator.CreateCallCount); - } + Assert.Equal(1, activator.CreateCallCount); + } - [Fact] - public async Task ActivateActorAsync_DoubleActivation_DeactivatesNewActor() - { - // We have to use the activator to observe the behavior here. We don't - // have a way to interact with the "new" actor that gets destroyed immediately. - var activator = new TestActivator(); + [Fact] + public async Task ActivateActorAsync_DoubleActivation_DeactivatesNewActor() + { + // We have to use the activator to observe the behavior here. We don't + // have a way to interact with the "new" actor that gets destroyed immediately. + var activator = new TestActivator(); - var manager = CreateActorManager(typeof(TestActor), activator); + var manager = CreateActorManager(typeof(TestActor), activator); - var id = ActorId.CreateRandom(); - await manager.ActivateActorAsync(id); + var id = ActorId.CreateRandom(); + await manager.ActivateActorAsync(id); - Assert.True(manager.TryGetActorAsync(id, out var original)); + Assert.True(manager.TryGetActorAsync(id, out var original)); - // It's a double-activation! We don't expect the runtime to do this, but the code - // handles it. - await manager.ActivateActorAsync(id); + // It's a double-activation! We don't expect the runtime to do this, but the code + // handles it. + await manager.ActivateActorAsync(id); - // Still holding the original actor - Assert.True(manager.TryGetActorAsync(id, out var another)); - Assert.Same(original, another); - Assert.False(Assert.IsType(another).IsDeactivated); - Assert.False(Assert.IsType(another).IsDisposed); + // Still holding the original actor + Assert.True(manager.TryGetActorAsync(id, out var another)); + Assert.Same(original, another); + Assert.False(Assert.IsType(another).IsDeactivated); + Assert.False(Assert.IsType(another).IsDisposed); - // We should have seen 2 create operations and 1 delete - Assert.Equal(2, activator.CreateCallCount); - Assert.Equal(1, activator.DeleteCallCount); - } + // We should have seen 2 create operations and 1 delete + Assert.Equal(2, activator.CreateCallCount); + Assert.Equal(1, activator.DeleteCallCount); + } - [Fact] - public async Task ActivateActorAsync_ExceptionDuringActivation_ActorNotStoredAndDeleted() - { - var activator = new TestActivator(); + [Fact] + public async Task ActivateActorAsync_ExceptionDuringActivation_ActorNotStoredAndDeleted() + { + var activator = new TestActivator(); - var manager = CreateActorManager(typeof(ThrowsDuringOnActivateAsync), activator); + var manager = CreateActorManager(typeof(ThrowsDuringOnActivateAsync), activator); - var id = ActorId.CreateRandom(); + var id = ActorId.CreateRandom(); - await Assert.ThrowsAsync(async () => - { - await manager.ActivateActorAsync(id); - }); + await Assert.ThrowsAsync(async () => + { + await manager.ActivateActorAsync(id); + }); - Assert.False(manager.TryGetActorAsync(id, out _)); - Assert.Equal(1, activator.DeleteCallCount); - } + Assert.False(manager.TryGetActorAsync(id, out _)); + Assert.Equal(1, activator.DeleteCallCount); + } - [Fact] - public async Task DectivateActorAsync_DeletesActorAndCallsDeactivateLifecycleMethod() - { - var manager = CreateActorManager(typeof(TestActor)); + [Fact] + public async Task DectivateActorAsync_DeletesActorAndCallsDeactivateLifecycleMethod() + { + var manager = CreateActorManager(typeof(TestActor)); - var id = ActorId.CreateRandom(); - await manager.ActivateActorAsync(id); + var id = ActorId.CreateRandom(); + await manager.ActivateActorAsync(id); - Assert.True(manager.TryGetActorAsync(id, out var actor)); - await manager.DeactivateActorAsync(id); + Assert.True(manager.TryGetActorAsync(id, out var actor)); + await manager.DeactivateActorAsync(id); - Assert.True(Assert.IsType(actor).IsDeactivated); - Assert.True(Assert.IsType(actor).IsDisposed); - } + Assert.True(Assert.IsType(actor).IsDeactivated); + Assert.True(Assert.IsType(actor).IsDisposed); + } - [Fact] - public async Task DeactivateActorAsync_ItsOkToDeactivateNonExistentActor() - { - var manager = CreateActorManager(typeof(TestActor)); + [Fact] + public async Task DeactivateActorAsync_ItsOkToDeactivateNonExistentActor() + { + var manager = CreateActorManager(typeof(TestActor)); - var id = ActorId.CreateRandom(); - Assert.False(manager.TryGetActorAsync(id, out _)); - await manager.DeactivateActorAsync(id); - } + var id = ActorId.CreateRandom(); + Assert.False(manager.TryGetActorAsync(id, out _)); + await manager.DeactivateActorAsync(id); + } - [Fact] - public async Task DeactivateActorAsync_UsesActivator() - { - var activator = new TestActivator(); + [Fact] + public async Task DeactivateActorAsync_UsesActivator() + { + var activator = new TestActivator(); - var manager = CreateActorManager(typeof(TestActor), activator); + var manager = CreateActorManager(typeof(TestActor), activator); - var id = ActorId.CreateRandom(); - await manager.ActivateActorAsync(id); - await manager.DeactivateActorAsync(id); + var id = ActorId.CreateRandom(); + await manager.ActivateActorAsync(id); + await manager.DeactivateActorAsync(id); - Assert.Equal(1, activator.CreateCallCount); - Assert.Equal(1, activator.DeleteCallCount); - } + Assert.Equal(1, activator.CreateCallCount); + Assert.Equal(1, activator.DeleteCallCount); + } - [Fact] - public async Task DeactivateActorAsync_ExceptionDuringDeactivation_ActorIsRemovedAndDeleted() - { - var activator = new TestActivator(); + [Fact] + public async Task DeactivateActorAsync_ExceptionDuringDeactivation_ActorIsRemovedAndDeleted() + { + var activator = new TestActivator(); - var manager = CreateActorManager(typeof(ThrowsDuringOnDeactivateAsync), activator); + var manager = CreateActorManager(typeof(ThrowsDuringOnDeactivateAsync), activator); - var id = ActorId.CreateRandom(); - await manager.ActivateActorAsync(id); - Assert.True(manager.TryGetActorAsync(id, out _)); + var id = ActorId.CreateRandom(); + await manager.ActivateActorAsync(id); + Assert.True(manager.TryGetActorAsync(id, out _)); - await Assert.ThrowsAsync(async () => - { - await manager.DeactivateActorAsync(id); - }); + await Assert.ThrowsAsync(async () => + { + await manager.DeactivateActorAsync(id); + }); - Assert.False(manager.TryGetActorAsync(id, out _)); - Assert.Equal(1, activator.DeleteCallCount); - } + Assert.False(manager.TryGetActorAsync(id, out _)); + Assert.Equal(1, activator.DeleteCallCount); + } + + private interface ITestActor : IActor { } - private interface ITestActor : IActor { } + private class TestActor : Actor, ITestActor, IDisposable + { + private static int counter; - private class TestActor : Actor, ITestActor, IDisposable + public TestActor(ActorHost host) : base(host) { - private static int counter; + Sequence = Interlocked.Increment(ref counter); + } - public TestActor(ActorHost host) : base(host) - { - Sequence = Interlocked.Increment(ref counter); - } + // Makes instances easier to tell apart for debugging. + public int Sequence { get; } - // Makes instances easier to tell apart for debugging. - public int Sequence { get; } + public bool IsActivated { get; set; } - public bool IsActivated { get; set; } + public bool IsDeactivated { get; set; } - public bool IsDeactivated { get; set; } + public bool IsDisposed { get; set; } - public bool IsDisposed { get; set; } + public void Dispose() + { + IsDisposed = true; + } - public void Dispose() - { - IsDisposed = true; - } + protected override Task OnActivateAsync() + { + IsActivated = true; + return Task.CompletedTask; + } - protected override Task OnActivateAsync() - { - IsActivated = true; - return Task.CompletedTask; - } + protected override Task OnDeactivateAsync() + { + IsDeactivated = true; + return Task.CompletedTask; + } + } - protected override Task OnDeactivateAsync() - { - IsDeactivated = true; - return Task.CompletedTask; - } + private class ThrowsDuringOnActivateAsync : Actor, ITestActor + { + public ThrowsDuringOnActivateAsync(ActorHost host) : base(host) + { } - private class ThrowsDuringOnActivateAsync : Actor, ITestActor + protected override Task OnActivateAsync() { - public ThrowsDuringOnActivateAsync(ActorHost host) : base(host) - { - } - - protected override Task OnActivateAsync() - { - throw new InvalidTimeZoneException(); - } + throw new InvalidTimeZoneException(); } + } - private class ThrowsDuringOnDeactivateAsync : Actor, ITestActor + private class ThrowsDuringOnDeactivateAsync : Actor, ITestActor + { + public ThrowsDuringOnDeactivateAsync(ActorHost host) : base(host) { - public ThrowsDuringOnDeactivateAsync(ActorHost host) : base(host) - { - } - - protected override Task OnDeactivateAsync() - { - throw new InvalidTimeZoneException(); - } } - private class TestActivator : DefaultActorActivator + protected override Task OnDeactivateAsync() { - public int CreateCallCount { get; set; } + throw new InvalidTimeZoneException(); + } + } - public int DeleteCallCount { get; set; } + private class TestActivator : DefaultActorActivator + { + public int CreateCallCount { get; set; } - public override Task CreateAsync(ActorHost host) - { - CreateCallCount++;; - return base.CreateAsync(host); - } + public int DeleteCallCount { get; set; } - public override Task DeleteAsync(ActorActivatorState state) - { - DeleteCallCount++; - return base.DeleteAsync(state); - } + public override Task CreateAsync(ActorHost host) + { + CreateCallCount++;; + return base.CreateAsync(host); + } + + public override Task DeleteAsync(ActorActivatorState state) + { + DeleteCallCount++; + return base.DeleteAsync(state); } } } diff --git a/test/Dapr.Actors.Test/Runtime/ActorStateTests.cs b/test/Dapr.Actors.Test/Runtime/ActorStateTests.cs new file mode 100644 index 000000000..918d0f6f5 --- /dev/null +++ b/test/Dapr.Actors.Test/Runtime/ActorStateTests.cs @@ -0,0 +1,358 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using Xunit; + +namespace Dapr.Actors.Runtime; + +public sealed class ActorStateTests +{ + [Fact] + public void ActorStateCache_Add_DoesNotContainAddsToState() + { + var cache = new ActorStateCache(); + const int value = 123; + var result = cache.Add("state", value); + + Assert.False(result.stateContainsKey); + Assert.True(result.addedToState); + } + + [Fact] + public void ActorStateCache_Add_AlreadyExists() + { + var cache = new ActorStateCache(); + const int value = 123; + const string stateName = "state"; + var state = ActorStateCache.StateMetadata.Create(value, StateChangeKind.Add); + cache.Set(stateName, state); + + var result = cache.Add(stateName, value); + + Assert.True(result.stateContainsKey); + Assert.False(result.addedToState); + } + + [Fact] + public void ActorStateCache_Add_MarkedAsRemoved() + { + var cache = new ActorStateCache(); + const int value = 123; + const string stateName = "state"; + var state = ActorStateCache.StateMetadata.Create(value, StateChangeKind.Remove); + cache.Set(stateName, state); + + var result = cache.Add(stateName, value, TimeSpan.FromMinutes(5)); + + Assert.True(result.stateContainsKey); + Assert.True(result.addedToState); + } + + [Fact] + public void ActorStateCache_AddExpiry_DoesNotContainAddsToState() + { + var cache = new ActorStateCache(); + const int value = 123; + var result = cache.Add("state", value, DateTimeOffset.UtcNow.AddMinutes(5)); + + Assert.False(result.stateContainsKey); + Assert.True(result.addedToState); + } + + [Fact] + public void ActorStateCache_AddExpiry_AlreadyExists() + { + var cache = new ActorStateCache(); + const int value = 123; + const string stateName = "state"; + var state = ActorStateCache.StateMetadata.Create(value, StateChangeKind.Add); + cache.Set(stateName, state); + + var result = cache.Add(stateName, value, DateTimeOffset.UtcNow.AddMinutes(5)); + + Assert.True(result.stateContainsKey); + Assert.False(result.addedToState); + } + + [Fact] + public void ActorStateCache_AddExpiry_MarkedAsRemoved() + { + var cache = new ActorStateCache(); + const int value = 123; + const string stateName = "state"; + var state = ActorStateCache.StateMetadata.Create(value, StateChangeKind.Remove); + cache.Set(stateName, state); + + var result = cache.Add(stateName, value, DateTimeOffset.UtcNow.AddMinutes(5)); + + Assert.True(result.stateContainsKey); + Assert.True(result.addedToState); + } + + [Fact] + public void ActorStateCache_Set() + { + var cache = new ActorStateCache(); + const string stateName = "state"; + const int value = 456; + const StateChangeKind kind = StateChangeKind.None; + var state = ActorStateCache.StateMetadata.Create(value, kind); + + cache.Set(stateName, state); + + var stateMetadata = cache.GetStateMetadata(); + Assert.Single(stateMetadata.Keys); + Assert.True(stateMetadata.ContainsKey(stateName)); + var data = stateMetadata[stateName]; + Assert.NotNull(data.Value); + Assert.Equal(value, data.Value); + Assert.Equal(kind, data.ChangeKind); + Assert.Null(data.TTLExpireTime); + Assert.Equal(value.GetType(), data.Type); + } + + [Fact] + public void ActorStateCache_Remove() + { + var cache = new ActorStateCache(); + const string stateName = "state"; + const int value = 456; + const StateChangeKind kind = StateChangeKind.None; + var state = ActorStateCache.StateMetadata.Create(value, kind); + + cache.Set(stateName, state); + + var stateMetadata = cache.GetStateMetadata(); + Assert.Single(stateMetadata.Keys); + Assert.True(stateMetadata.ContainsKey(stateName)); + + cache.Remove(stateName); + stateMetadata = cache.GetStateMetadata(); + Assert.Empty(stateMetadata.Keys); + } + + [Fact] + public void ActorStateCache_TryGet_ExistsWithoutExpiration() + { + var cache = new ActorStateCache(); + const int value = 123; + const StateChangeKind kind = StateChangeKind.Add; + const string stateName = "state"; + var state = ActorStateCache.StateMetadata.Create(value, kind); + + cache.Set(stateName, state); + + var result = cache.TryGet(stateName, out var retrievedState); + Assert.True(result.containsKey); + Assert.False(result.isMarkedAsRemoveOrExpired); + Assert.NotNull(retrievedState); + Assert.NotNull(retrievedState.Value); + Assert.Equal(value, (int)retrievedState.Value); + Assert.Equal(kind, retrievedState.ChangeKind); + Assert.Null(retrievedState.TTLExpireTime); + Assert.Equal(value.GetType(), retrievedState.Type); + } + + [Fact] + public void ActorStateCache_TryGet_DoesNotExist() + { + var cache = new ActorStateCache(); + var result = cache.TryGet("mystate", out var retrievedState); + + Assert.False(result.containsKey); + Assert.False(result.isMarkedAsRemoveOrExpired); + Assert.Null(retrievedState); + } + + [Fact] + public void ActorStateCache_TryGet_IsExpired() + { + var cache = new ActorStateCache(); + const string value = "acb"; + const StateChangeKind kind = StateChangeKind.Update; + const string stateName = "state"; + var state = ActorStateCache.StateMetadata.Create(value, kind, DateTimeOffset.UtcNow.AddMinutes(-10)); + + cache.Set(stateName, state); + + var result = cache.TryGet(stateName, out var retrievedState); + Assert.True(result.containsKey); + Assert.True(result.isMarkedAsRemoveOrExpired); + Assert.NotNull(retrievedState); + Assert.NotNull(retrievedState.Value); + Assert.Equal(value, (string)retrievedState.Value); + Assert.Equal(kind, retrievedState.ChangeKind); + Assert.NotNull(retrievedState.TTLExpireTime); + Assert.Equal(value.GetType(), retrievedState.Type); + } + + [Fact] + public void ActorStateCache_Clear() + { + var cache = new ActorStateCache(); + cache.Add("state1", 123); + cache.Add("state2", "456"); + cache.Add("state3", 7890); + + var data = cache.GetStateMetadata(); + Assert.Equal(3, data.Keys.Count); + Assert.Contains("state1", data.Keys); + Assert.Contains("state2", data.Keys); + Assert.Contains("state3", data.Keys); + + cache.Clear(); + data = cache.GetStateMetadata(); + Assert.Empty(data.Keys); + } + + [Fact] + public void ActorStateCache_BuildChangeList_NoChanges() + { + var cache = new ActorStateCache(); + var result = cache.BuildChangeList(); + + Assert.Empty(result.stateChanges); + Assert.Empty(result.statesToRemove); + } + + [Fact] + public void ActorStateCache_BuildChangeList_ChangesWithRemovals() + { + var cache = new ActorStateCache(); + cache.Add("state1", 456); + var state2Offset = DateTimeOffset.UtcNow.AddMinutes(15); + cache.Set("state2", ActorStateCache.StateMetadata.Create("78", StateChangeKind.Remove, state2Offset)); + cache.Add("state3", "test"); + + var (stateChanges, removalChanges) = cache.BuildChangeList(); + + //Validate stateChanges + Assert.Equal(3, stateChanges.Count); + Assert.Contains(new ActorStateChange("state1", typeof(int), 456, StateChangeKind.Add, null), stateChanges); + Assert.Contains(new ActorStateChange("state2", typeof(string), "78", StateChangeKind.Remove, state2Offset), stateChanges); + Assert.Contains(new ActorStateChange("state3", typeof(string), "test", StateChangeKind.Add, null), + stateChanges); + + //Validate removalChanges + Assert.Single(removalChanges); + Assert.Contains("state2", removalChanges); + + //Validate every state value was marked as None + var states = cache.GetStateMetadata(); + Assert.Equal(3, states.Count); + foreach (var state in states) + { + Assert.Equal(StateChangeKind.None, state.Value.ChangeKind); + } + } + + [Fact] + public void ActorStateCache_ShouldNotBeMarkedAsRemovedOrExpired() + { + var cache = new ActorStateCache(); + var state = ActorStateCache.StateMetadata.Create(123, StateChangeKind.Update, DateTimeOffset.UtcNow.AddMinutes(10)); + var result = cache.IsMarkedAsRemoveOrExpired(state); + + Assert.False(result); + } + + [Fact] + public void ActorStateCache_ShouldBeMarkedAsRemoved() + { + var cache = new ActorStateCache(); + var state = ActorStateCache.StateMetadata.Create(123, StateChangeKind.Remove); + var result = cache.IsMarkedAsRemoveOrExpired(state); + + Assert.True(result); + } + + [Fact] + public void ActorStateCache_ShouldBeMarkedAsExpired() + { + var cache = new ActorStateCache(); + var state = ActorStateCache.StateMetadata.Create(123, StateChangeKind.Update, DateTimeOffset.UtcNow.AddMinutes(-10)); + var result = cache.IsMarkedAsRemoveOrExpired(state); + + Assert.True(result); + } + + [Fact] + public void StateMetadata_ShouldThrowIfBothTtlExpireTimeAndTtlAreSet() + { + Assert.Throws(() => + { + // ReSharper disable once ObjectCreationAsStatement + new ActorStateCache.StateMetadata("123", typeof(int), StateChangeKind.None, + DateTimeOffset.UtcNow, + TimeSpan.FromMinutes(5)); + }); + } + + [Fact] + public void StateMetadata_CreatePlain() + { + const int stateValue = 123; + var type = stateValue.GetType(); + const StateChangeKind kind = StateChangeKind.None; + var data = ActorStateCache.StateMetadata.Create(stateValue, kind); + + Assert.NotNull(data.Value); + Assert.Equal(stateValue, (int)data.Value); + Assert.Equal(type, data.Type); + Assert.Equal(kind, data.ChangeKind); + Assert.Null(data.TTLExpireTime); + } + + [Fact] + public void StateMetadata_CreateWithTtl() + { + const int stateValue = 123; + var type = stateValue.GetType(); + const StateChangeKind kind = StateChangeKind.Add; + var ttl = TimeSpan.FromMinutes(10); + var data = ActorStateCache.StateMetadata.Create(stateValue, kind, ttl); + + Assert.NotNull(data.Value); + Assert.Equal(stateValue, (int)data.Value); + Assert.Equal(type, data.Type); + Assert.Equal(kind, data.ChangeKind); + Assert.NotNull(data.TTLExpireTime); + } + + [Fact] + public void StateMetadata_CreateWithTtlExpiryTime() + { + const int stateValue = 123; + var type = stateValue.GetType(); + const StateChangeKind kind = StateChangeKind.Add; + var ttlExpiry = DateTimeOffset.UtcNow.AddMinutes(5); + var data = ActorStateCache.StateMetadata.Create(stateValue, kind, ttlExpiry); + + Assert.NotNull(data.Value); + Assert.Equal(stateValue, (int)data.Value); + Assert.Equal(type, data.Type); + Assert.Equal(kind, data.ChangeKind); + Assert.Equal(ttlExpiry, data.TTLExpireTime); + } + + [Fact] + public void StateMetadata_CreateForRemoval() + { + var data = ActorStateCache.StateMetadata.CreateForRemove(); + + Assert.Null(data.Value); + Assert.Null(data.TTLExpireTime); + Assert.Equal(StateChangeKind.Remove, data.ChangeKind); + } +} diff --git a/test/Dapr.Actors.Test/TestDaprInteractor.cs b/test/Dapr.Actors.Test/TestDaprInteractor.cs index 0ae6b7622..5f36dfc43 100644 --- a/test/Dapr.Actors.Test/TestDaprInteractor.cs +++ b/test/Dapr.Actors.Test/TestDaprInteractor.cs @@ -1,4 +1,5 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -59,7 +60,7 @@ public Task InvokeActorMethodWithoutRemotingAsync(string actorType, stri { throw new System.NotImplementedException(); } - + /// /// Saves state batch to Dapr. /// @@ -80,11 +81,39 @@ public virtual async Task SaveStateTransactionallyAsync(string actorType, string /// Type of actor. /// ActorId. /// Name of key to get value for. + /// The data to persist to state. + /// Cancels the operation. + /// A task that represents the asynchronous operation. + public virtual async Task SaveStateAsync( + string actorType, + string actorId, + string keyName, + string data, + CancellationToken cancellationToken = default) + { + await _testDaprInteractor.SaveStateAsync(actorType, actorId, keyName, data, cancellationToken); + } + + /// + /// Gets a state from Dapr. + /// + /// Type of actor. + /// ActorId. + /// Name of key to get value for. /// Cancels the operation. /// A task that represents the asynchronous operation. public virtual async Task> GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default) { - return await _testDaprInteractor.GetStateAsync(actorType, actorId, keyName); + return await _testDaprInteractor.GetStateAsync(actorType, actorId, keyName, cancellationToken); + } + + public virtual async Task>> GetListStateAsync( + string actorType, + string actorId, + string keyName, + CancellationToken cancellationToken = default) + { + return await _testDaprInteractor.GetListStateAsync(actorType, actorId, keyName, cancellationToken); } /// diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs index 5a20fff3d..26f01fb5a 100644 --- a/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs +++ b/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs @@ -97,7 +97,7 @@ public async Task ActorCanSupportCustomSerializer() /// . (it's defined in the base of it.) /// That why was created, /// so there are now more then one method. - /// + /// [Fact] public async Task ActorCanSupportCustomSerializerAndCallMoreThenOneDefinedMethod() { diff --git a/test/Dapr.E2E.Test/DaprTestApp.cs b/test/Dapr.E2E.Test/DaprTestApp.cs index 2330785d8..8776a43d5 100644 --- a/test/Dapr.E2E.Test/DaprTestApp.cs +++ b/test/Dapr.E2E.Test/DaprTestApp.cs @@ -45,7 +45,7 @@ public DaprTestApp(ITestOutputHelper output, string appId) { var (appPort, httpPort, grpcPort, metricsPort) = GetFreePorts(); - var componentsPath = Combine(".", "..", "..", "..", "..", "..", "test", "Dapr.E2E.Test", "components"); + var resourcesPath = Combine(".", "..", "..", "..", "..", "..", "test", "Dapr.E2E.Test", "components"); var configPath = Combine(".", "..", "..", "..", "..", "..", "test", "Dapr.E2E.Test", "configuration", "featureconfig.yaml"); var arguments = new List() { @@ -55,11 +55,10 @@ public DaprTestApp(ITestOutputHelper output, string appId) "--dapr-http-port", httpPort.ToString(CultureInfo.InvariantCulture), "--dapr-grpc-port", grpcPort.ToString(CultureInfo.InvariantCulture), "--metrics-port", metricsPort.ToString(CultureInfo.InvariantCulture), - "--components-path", componentsPath, + "--resources-path", resourcesPath, "--config", configPath, "--log-level", "debug", - "--dapr-http-max-request-size", "32", - + "--max-body-size", "8Mi" }; if (configuration.UseAppPort)