From ebf65c5161bf5b6338cb3666b9fe637c2709d033 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 22 Feb 2025 08:34:44 -0600 Subject: [PATCH 01/13] Implemented standalone actor state cache Signed-off-by: Whit Waldo --- src/Dapr.Actors/Runtime/ActorStateCache.cs | 224 ++++++++++++++++++++ src/Dapr.Actors/Runtime/IActorStateCache.cs | 85 ++++++++ 2 files changed, 309 insertions(+) create mode 100644 src/Dapr.Actors/Runtime/ActorStateCache.cs create mode 100644 src/Dapr.Actors/Runtime/IActorStateCache.cs 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/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); +} From 9f34faf297a41490c7c6cb90a7668ec7f43d3408 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 22 Feb 2025 08:35:15 -0600 Subject: [PATCH 02/13] Updated to use record instead of unnecessary class, decorated with nullable annotation Signed-off-by: Whit Waldo --- src/Dapr.Actors/Runtime/ActorStateChange.cs | 93 +++++---------------- 1 file changed, 20 insertions(+), 73 deletions(-) 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); From 901d14cd06ebbb52e6f7ea3bf3bd62978f700bd1 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 22 Feb 2025 08:35:47 -0600 Subject: [PATCH 03/13] Updated to utilize new standalone actor cache manager Signed-off-by: Whit Waldo --- src/Dapr.Actors/Runtime/ActorStateManager.cs | 754 ++++++++----------- 1 file changed, 319 insertions(+), 435 deletions(-) diff --git a/src/Dapr.Actors/Runtime/ActorStateManager.cs b/src/Dapr.Actors/Runtime/ActorStateManager.cs index 31ada4433..bb65810ea 100644 --- a/src/Dapr.Actors/Runtime/ActorStateManager.cs +++ b/src/Dapr.Actors/Runtime/ActorStateManager.cs @@ -19,567 +19,451 @@ 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]; + if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) + { + 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; - } + 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; - } + 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; } From b280c702a49d9381b844f5970807a661013ed814 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 22 Feb 2025 08:36:25 -0600 Subject: [PATCH 04/13] Implemented unit tests for the ActorStateCache and included types Signed-off-by: Whit Waldo --- .../Runtime/ActorStateTests.cs | 358 ++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 test/Dapr.Actors.Test/Runtime/ActorStateTests.cs 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); + } +} From 8f57b35cd515bf34d3d465fff866d59b93345d76 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 22 Feb 2025 08:38:34 -0600 Subject: [PATCH 05/13] Adding infrastructure to support building out richer unit tests of the actor state manager and verifying that it's using the standalone actor cache properly. Signed-off-by: Whit Waldo --- src/Dapr.Actors/IDaprInteractor.cs | 24 ++ .../Dapr.Actors.Test/ActorStateManagerTest.cs | 361 +++++++++--------- .../Runtime/ActorManagerTests.cs | 345 +++++++++-------- test/Dapr.Actors.Test/TestDaprInteractor.cs | 35 +- 4 files changed, 414 insertions(+), 351 deletions(-) diff --git a/src/Dapr.Actors/IDaprInteractor.cs b/src/Dapr.Actors/IDaprInteractor.cs index 5849328a8..805bbc7a1 100644 --- a/src/Dapr.Actors/IDaprInteractor.cs +++ b/src/Dapr.Actors/IDaprInteractor.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Collections.Generic; + namespace Dapr.Actors { using System.IO; @@ -50,10 +52,32 @@ internal interface IDaprInteractor /// 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. + Task SaveStateAsync( + string actorType, + string actorId, + string keyName, + string data, + 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); + Task>> GetListStateAsync( + string actorType, + string actorId, + string keyName, + CancellationToken cancellationToken = default); + /// /// Invokes Actor method. /// diff --git a/test/Dapr.Actors.Test/ActorStateManagerTest.cs b/test/Dapr.Actors.Test/ActorStateManagerTest.cs index a4e0e4140..e43b75ba5 100644 --- a/test/Dapr.Actors.Test/ActorStateManagerTest.cs +++ b/test/Dapr.Actors.Test/ActorStateManagerTest.cs @@ -11,182 +11,193 @@ // 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 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)); + } + + // [Fact] + // public async Task AddStateAsync_NoTTL() + // { + // 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(); + // + // + // } } 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/TestDaprInteractor.cs b/test/Dapr.Actors.Test/TestDaprInteractor.cs index 2d81f16d0..d706520ac 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.Threading; using System.Threading.Tasks; using Dapr.Actors.Communication; @@ -58,7 +59,7 @@ public Task InvokeActorMethodWithoutRemotingAsync(string actorType, stri { throw new System.NotImplementedException(); } - + /// /// Saves state batch to Dapr. /// @@ -79,11 +80,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); } /// From d332d28b1e3b9301c47e8c06fb7e23e50b027890 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 22 Feb 2025 09:00:28 -0600 Subject: [PATCH 06/13] Fixed build error Signed-off-by: Whit Waldo --- src/Dapr.Actors/IDaprInteractor.cs | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/Dapr.Actors/IDaprInteractor.cs b/src/Dapr.Actors/IDaprInteractor.cs index 805bbc7a1..5573b63c0 100644 --- a/src/Dapr.Actors/IDaprInteractor.cs +++ b/src/Dapr.Actors/IDaprInteractor.cs @@ -46,22 +46,6 @@ internal interface IDaprInteractor /// 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. - /// The data to persist to state. - /// Cancels the operation. - /// A task that represents the asynchronous operation. - Task SaveStateAsync( - string actorType, - string actorId, - string keyName, - string data, - CancellationToken cancellationToken = default); - /// /// Gets a state from Dapr. /// @@ -72,12 +56,6 @@ Task SaveStateAsync( /// A task that represents the asynchronous operation. Task> GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default); - Task>> GetListStateAsync( - string actorType, - string actorId, - string keyName, - CancellationToken cancellationToken = default); - /// /// Invokes Actor method. /// From 60cd82de03d4a67b4ee00a039d4231eb4514bdda Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 22 Feb 2025 19:03:09 -0600 Subject: [PATCH 07/13] Fixed breaking unit test - some assumptions had changed around the cache order Signed-off-by: Whit Waldo --- src/Dapr.Actors/Runtime/ActorStateManager.cs | 9 ++++++- .../Dapr.Actors.Test/ActorStateManagerTest.cs | 25 +++++++++++-------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/Dapr.Actors/Runtime/ActorStateManager.cs b/src/Dapr.Actors/Runtime/ActorStateManager.cs index bb65810ea..b496f1589 100644 --- a/src/Dapr.Actors/Runtime/ActorStateManager.cs +++ b/src/Dapr.Actors/Runtime/ActorStateManager.cs @@ -75,11 +75,16 @@ public async Task TryAddStateAsync(string stateName, T value, Cancellat return addedToState; } - if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) + 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; } + //Add to the cache + cache.Set(stateName, ActorStateCache.StateMetadata.Create(value, StateChangeKind.Add)); return addedToState; } @@ -101,6 +106,8 @@ public async Task TryAddStateAsync(string stateName, T value, TimeSpan return false; } + //Add to the cache + cache.Set(stateName, ActorStateCache.StateMetadata.Create(value, StateChangeKind.Add, ttl)); return addedToState; } diff --git a/test/Dapr.Actors.Test/ActorStateManagerTest.cs b/test/Dapr.Actors.Test/ActorStateManagerTest.cs index e43b75ba5..9c253b6e4 100644 --- a/test/Dapr.Actors.Test/ActorStateManagerTest.cs +++ b/test/Dapr.Actors.Test/ActorStateManagerTest.cs @@ -133,15 +133,16 @@ public async Task StateDaprdExpireTime() var host = ActorHost.CreateForTest(); host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); var mngr = new ActorStateManager(new TestActor(host)); - var token = new CancellationToken(); + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - // Existing key which has an expiry time. + // 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())) + .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)); + + 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 @@ -149,11 +150,13 @@ public async Task StateDaprdExpireTime() .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)); + 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] From 5055ef8dd4634f951006a1f7f1d7ed360353ab90 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 22 Feb 2025 19:15:15 -0600 Subject: [PATCH 08/13] Updated existing actor state manager tests Signed-off-by: Whit Waldo --- .../Dapr.Actors.Test/ActorStateManagerTest.cs | 115 ++++++++++-------- 1 file changed, 67 insertions(+), 48 deletions(-) diff --git a/test/Dapr.Actors.Test/ActorStateManagerTest.cs b/test/Dapr.Actors.Test/ActorStateManagerTest.cs index 9c253b6e4..314efb8d6 100644 --- a/test/Dapr.Actors.Test/ActorStateManagerTest.cs +++ b/test/Dapr.Actors.Test/ActorStateManagerTest.cs @@ -35,24 +35,33 @@ public async Task SetGet() var host = ActorHost.CreateForTest(); host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); var mngr = new ActorStateManager(new TestActor(host)); - var token = new CancellationToken(); + 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))); - 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)); + 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] @@ -62,30 +71,35 @@ public async Task StateWithTTL() var host = ActorHost.CreateForTest(); host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); var mngr = new ActorStateManager(new TestActor(host)); - var token = new CancellationToken(); + 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))); - 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)); + 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)); + await Task.Delay(TimeSpan.FromSeconds(1.5), cts.Token); - await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token)); - await Assert.ThrowsAsync(() => mngr.GetStateAsync("key2", 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", "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)); + 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] @@ -95,39 +109,44 @@ public async Task StateRemoveAddTTL() var host = ActorHost.CreateForTest(); host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); var mngr = new ActorStateManager(new TestActor(host)); - var token = new CancellationToken(); + 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))); - 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)); + 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", "value1", token); - await mngr.SetStateAsync("key2", "value2", token); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", 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)); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", 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)); // 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)); + 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 StateDaprdExpireTime() + public async Task ValidateStateExpirationAndExceptions() { var interactor = new Mock(); var host = ActorHost.CreateForTest(); From ed696baaa048ee4d97e7428f41fdf8027a8e8215 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 26 Feb 2025 03:56:41 -0600 Subject: [PATCH 09/13] Migrating test out to E2E tests Signed-off-by: Whit Waldo --- test/Dapr.Actors.Test/ActorStateManagerTest.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/test/Dapr.Actors.Test/ActorStateManagerTest.cs b/test/Dapr.Actors.Test/ActorStateManagerTest.cs index 314efb8d6..402d7474f 100644 --- a/test/Dapr.Actors.Test/ActorStateManagerTest.cs +++ b/test/Dapr.Actors.Test/ActorStateManagerTest.cs @@ -210,16 +210,4 @@ public async Task RemoveState() Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); } - - // [Fact] - // public async Task AddStateAsync_NoTTL() - // { - // 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(); - // - // - // } } From dffc3053151e06210abc46a3e8336873d39d4c88 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 26 Feb 2025 04:02:03 -0600 Subject: [PATCH 10/13] Fixed build error introduced by bad merge Signed-off-by: Whit Waldo --- src/Dapr.Actors/IDaprInteractor.cs | 188 ++++++++++++++--------------- 1 file changed, 94 insertions(+), 94 deletions(-) diff --git a/src/Dapr.Actors/IDaprInteractor.cs b/src/Dapr.Actors/IDaprInteractor.cs index 6a1dd6a14..9e7cf1e59 100644 --- a/src/Dapr.Actors/IDaprInteractor.cs +++ b/src/Dapr.Actors/IDaprInteractor.cs @@ -11,109 +11,109 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors -{ - using System.IO; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Actors.Communication; - using System.Collections.Generic; +namespace Dapr.Actors; + +using System.Net.Http; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors.Communication; +using System.Collections.Generic; +/// +/// Interface for interacting with Dapr runtime. +/// +internal interface IDaprInteractor +{ /// - /// 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); - /// - /// 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); + /// + /// 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); } From 8aac15451ffcecb6fc5553cf4869f0d050d03bef Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 26 Feb 2025 04:02:39 -0600 Subject: [PATCH 11/13] Removed unused using Signed-off-by: Whit Waldo --- src/Dapr.Actors/IDaprInteractor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Dapr.Actors/IDaprInteractor.cs b/src/Dapr.Actors/IDaprInteractor.cs index 9e7cf1e59..dde86b918 100644 --- a/src/Dapr.Actors/IDaprInteractor.cs +++ b/src/Dapr.Actors/IDaprInteractor.cs @@ -18,7 +18,6 @@ namespace Dapr.Actors; using System.Threading; using System.Threading.Tasks; using Dapr.Actors.Communication; -using System.Collections.Generic; /// /// Interface for interacting with Dapr runtime. From 596544dcd7c926a29539241b89ac2fabd36b9751 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 26 Feb 2025 04:03:02 -0600 Subject: [PATCH 12/13] Added missing using Signed-off-by: Whit Waldo --- test/Dapr.Actors.Test/TestDaprInteractor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Dapr.Actors.Test/TestDaprInteractor.cs b/test/Dapr.Actors.Test/TestDaprInteractor.cs index 008431e08..5f36dfc43 100644 --- a/test/Dapr.Actors.Test/TestDaprInteractor.cs +++ b/test/Dapr.Actors.Test/TestDaprInteractor.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Dapr.Actors.Communication; From 55895fa19dc3aae8e19760d10029dca0541023bd Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 16 Mar 2025 20:14:32 -0500 Subject: [PATCH 13/13] Updated Dapr runtime/CLI version used in integration tests (#1485) * Updated itests.yml to use latest 1.15 runtime and CLI versions over 1.14 versions * Updated CLI argument name as `dapr-http-max-request-size` was changed in a recent update included in the CLI Signed-off-by: Whit Waldo --- .github/workflows/itests.yml | 4 ++-- .../SecretStoreConfigurationProviderSample/README.md | 2 +- examples/Client/ConfigurationApi/README.md | 2 +- examples/Client/DistributedLock/README.md | 6 +++--- .../Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs | 2 +- test/Dapr.E2E.Test/DaprTestApp.cs | 7 +++---- 6 files changed, 11 insertions(+), 12 deletions(-) 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/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)