Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewriting actor state caching mechanism #1473

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
4 changes: 3 additions & 1 deletion src/Dapr.Actors/IDaprInteractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
// limitations under the License.
// ------------------------------------------------------------------------

using System.Collections.Generic;

namespace Dapr.Actors
{
using System.IO;
Expand Down Expand Up @@ -45,7 +47,7 @@ internal interface IDaprInteractor
Task SaveStateTransactionallyAsync(string actorType, string actorId, string data, CancellationToken cancellationToken = default);

/// <summary>
/// Saves a state to Dapr.
/// Gets a state from Dapr.
/// </summary>
/// <param name="actorType">Type of actor.</param>
/// <param name="actorId">ActorId.</param>
Expand Down
224 changes: 224 additions & 0 deletions src/Dapr.Actors/Runtime/ActorStateCache.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Maintains the cache state.
/// </summary>
private readonly Dictionary<string, StateMetadata> stateMetadata = new();

/// <summary>
/// Adds the indicated value to the cache.
/// </summary>
/// <param name="stateName">The name of the state.</param>
/// <param name="value">The cached value.</param>
/// <param name="ttl">How far out the TTL expiry should be.</param>
/// <typeparam name="T">The type of value getting cached.</typeparam>
/// <returns><c>stateContainsKey</c> indicates if the cache already contains the key or not and
/// <c>addedToState</c> indicates if the value was added or updated in the cache.</returns>
public (bool stateContainsKey, bool addedToState) Add<T>(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);
}

/// <summary>
/// Adds the indicated value to the cache.
/// </summary>
/// <param name="stateName">The name of the state.</param>
/// <param name="value">The cached value.</param>
/// <param name="ttlExpiry">The TTL expiry timestamp.</param>
/// <typeparam name="T">The type of value getting cached.</typeparam>
/// <returns><c>stateContainsKey</c> indicates if the cache already contains the key or not and
/// <c>addedToState</c> indicates if the value was added or updated in the cache.</returns>
public (bool stateContainsKey, bool addedToState) Add<T>(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);

}

/// <summary>
/// Sets the cache with the specified value whether it already exists or not.
/// </summary>
/// <param name="stateName">The name of the state to save the value to.</param>
/// <param name="metadata">The state metadata to save to the cache.</param>
public void Set(string stateName, StateMetadata metadata)
{
stateMetadata[stateName] = metadata;
}

/// <summary>
/// Removes the indicated state name from the cache.
/// </summary>
/// <param name="stateName">The name of the state to remove.</param>
public void Remove(string stateName) => stateMetadata.Remove(stateName);

/// <summary>
/// Retrieves the current state from the cache if available and not expired.
/// </summary>
/// <param name="stateName">The name of the state to retrieve.</param>
/// <param name="metadata">If available and not expired, the value of the state persisted in the cache.</param>
/// <returns>True if the cache contains the state name; false if not.</returns>
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);

}

/// <summary>
/// Clears the all the data from the cache.
/// </summary>
public void Clear()
{
stateMetadata.Clear();
}

/// <summary>
/// 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 <c>SaveStateAsync</c> method in <see cref="ActorStateManager"/>.
/// </summary>
/// <returns>The list of state changes and states to remove from the cache.</returns>
public (IReadOnlyList<ActorStateChange> stateChanges, IReadOnlyList<string> statesToRemove) BuildChangeList()
{
var stateChanges = new List<ActorStateChange>();
var statesToRemove = new List<string>();

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);
}

/// <summary>
/// Helper method that determines if a state metadata is expired.
/// </summary>
/// <param name="metadata">The metadata to evaluate.</param>
/// <returns>True if the state metadata is marked for removal or the TTL has expired, otherwise false.</returns>
public bool IsMarkedAsRemoveOrExpired(StateMetadata metadata) =>
metadata.ChangeKind == StateChangeKind.Remove || (metadata.TTLExpireTime.HasValue &&
metadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow);

/// <summary>
/// Exposed for testing only.
/// </summary>
/// <returns></returns>
internal Dictionary<string, StateMetadata> GetStateMetadata() => stateMetadata;

internal sealed record StateMetadata
{
/// <summary>
/// This should only be used for testing purposes. Use the static `Create` methods for any actual usage.
/// </summary>
/// <param name="value"></param>
/// <param name="type"></param>
/// <param name="changeKind"></param>
/// <param name="ttlExpireTime"></param>
/// <param name="ttl"></param>
/// <exception cref="ArgumentException"></exception>
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>(T? value, StateChangeKind changeKind) =>
new(value, typeof(T), changeKind);

public static StateMetadata Create<T>(T? value, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime) =>
new(value, typeof(T), changeKind, ttlExpireTime: ttlExpireTime);

public static StateMetadata Create<T>(T? value, StateChangeKind changeKind, TimeSpan? ttl) =>
new(value, typeof(T), changeKind, ttl: ttl);

public static StateMetadata CreateForRemove() => new(null, typeof(object), StateChangeKind.Remove);
}
}
93 changes: 20 additions & 73 deletions src/Dapr.Actors/Runtime/ActorStateChange.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,75 +11,22 @@
// limitations under the License.
// ------------------------------------------------------------------------

namespace Dapr.Actors.Runtime
{
using System;

/// <summary>
/// Represents a change to an actor state with a given state name.
/// </summary>
public sealed class ActorStateChange
{
/// <summary>
/// Initializes a new instance of the <see cref="ActorStateChange"/> class.
/// </summary>
/// <param name="stateName">The name of the actor state.</param>
/// <param name="type">The type of value associated with given actor state name.</param>
/// <param name="value">The value associated with given actor state name.</param>
/// <param name="changeKind">The kind of state change for given actor state name.</param>
/// <param name="ttlExpireTime">The time to live for the state.</param>
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;
}

/// <summary>
/// Gets the name of the actor state.
/// </summary>
/// <value>
/// The name of the actor state.
/// </value>
public string StateName { get; }

/// <summary>
/// Gets the type of value associated with given actor state name.
/// </summary>
/// <value>
/// The type of value associated with given actor state name.
/// </value>
public Type Type { get; }

/// <summary>
/// Gets the value associated with given actor state name.
/// </summary>
/// <value>
/// The value associated with given actor state name.
/// </value>
public object Value { get; }

/// <summary>
/// Gets the kind of state change for given actor state name.
/// </summary>
/// <value>
/// The kind of state change for given actor state name.
/// </value>
public StateChangeKind ChangeKind { get; }

/// <summary>
/// Gets the time to live for the state.
/// </summary>
/// <value>
/// The time to live for the state.
/// </value>
/// <remarks>
/// If null, the state will not expire.
/// </remarks>
public DateTimeOffset? TTLExpireTime { get; }
}
}
#nullable enable
namespace Dapr.Actors.Runtime;

using System;

/// <summary>
/// Represents a change to an actor state with a given state name.
/// </summary>
/// <param name="StateName">The name of the actor state.</param>
/// <param name="Type">The type of value associated with the given actor state name.</param>
/// <param name="Value">The value associated with the given actor state name.</param>
/// <param name="ChangeKind">The kind of state change for the given actor state name.</param>
/// <param name="TTLExpireTime">The time to live for the state. If null, the state wil not expire.</param>
public sealed record ActorStateChange(
string StateName,
Type Type,
object? Value,
StateChangeKind ChangeKind,
DateTimeOffset? TTLExpireTime);
Loading
Loading