From 32d06a7136a4e32a70e02398619442d28f120165 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 8 Apr 2025 00:17:49 -0500 Subject: [PATCH] Tentative fix for timers deserializing error (#1512) * Tentative fix for deserializing error * Added unit tests to prove out timer deserialization for all supported formats Signed-off-by: Whit Waldo --- src/Dapr.Actors/Runtime/ActorManager.cs | 58 ++++++- src/Dapr.Actors/Runtime/ConverterUtils.cs | 2 +- .../Runtime/ActorManagerTests.cs | 156 ++++++++++++++++++ 3 files changed, 214 insertions(+), 2 deletions(-) diff --git a/src/Dapr.Actors/Runtime/ActorManager.cs b/src/Dapr.Actors/Runtime/ActorManager.cs index c78126cc..5b076874 100644 --- a/src/Dapr.Actors/Runtime/ActorManager.cs +++ b/src/Dapr.Actors/Runtime/ActorManager.cs @@ -223,7 +223,7 @@ namespace Dapr.Actors.Runtime internal async Task FireTimerAsync(ActorId actorId, Stream requestBodyStream, CancellationToken cancellationToken = default) { #pragma warning disable 0618 - var timerData = await JsonSerializer.DeserializeAsync(requestBodyStream); + var timerData = await DeserializeAsync(requestBodyStream); #pragma warning restore 0618 // Create a Func to be invoked by common method. @@ -243,6 +243,62 @@ namespace Dapr.Actors.Runtime await this.DispatchInternalAsync(actorId, this.timerMethodContext, RequestFunc, cancellationToken); } +#pragma warning disable 0618 + internal static async Task DeserializeAsync(Stream stream) + { + var json = await JsonSerializer.DeserializeAsync(stream); + if (json.ValueKind == JsonValueKind.Null) + { + return null; + } + + var setAnyProperties = false; // Used to determine if anything was actually deserialized + var dueTime = TimeSpan.Zero; + var callback = ""; + var period = TimeSpan.Zero; + var data = Array.Empty(); + TimeSpan? ttl = null; + if (json.TryGetProperty("callback", out var callbackProperty)) + { + setAnyProperties = true; + callback = callbackProperty.GetString(); + } + if (json.TryGetProperty("dueTime", out var dueTimeProperty)) + { + setAnyProperties = true; + var dueTimeString = dueTimeProperty.GetString(); + dueTime = ConverterUtils.ConvertTimeSpanFromDaprFormat(dueTimeString); + } + + if (json.TryGetProperty("period", out var periodProperty)) + { + setAnyProperties = true; + var periodString = periodProperty.GetString(); + (period, _) = ConverterUtils.ConvertTimeSpanValueFromISO8601Format(periodString); + } + + if (json.TryGetProperty("data", out var dataProperty) && dataProperty.ValueKind != JsonValueKind.Null) + { + setAnyProperties = true; + data = dataProperty.GetBytesFromBase64(); + } + + if (json.TryGetProperty("ttl", out var ttlProperty)) + { + setAnyProperties = true; + var ttlString = ttlProperty.GetString(); + ttl = ConverterUtils.ConvertTimeSpanFromDaprFormat(ttlString); + } + + if (!setAnyProperties) + { + return null; //No properties were ever deserialized, so return null instead of default values + } + + return new TimerInfo(callback, data, dueTime, period, ttl); + } +#pragma warning restore 0618 + internal async Task ActivateActorAsync(ActorId actorId) { // An actor is activated by "Dapr" runtime when a call is to be made for an actor. diff --git a/src/Dapr.Actors/Runtime/ConverterUtils.cs b/src/Dapr.Actors/Runtime/ConverterUtils.cs index 94cfd3d3..7c04bb49 100644 --- a/src/Dapr.Actors/Runtime/ConverterUtils.cs +++ b/src/Dapr.Actors/Runtime/ConverterUtils.cs @@ -103,7 +103,7 @@ internal static class ConverterUtils builder.Append($"{value.Days}D"); } - builder.Append("T"); + builder.Append('T'); if(value.Hours > 0) { diff --git a/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs b/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs index b27e9afe..82ec4189 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs @@ -12,6 +12,8 @@ // ------------------------------------------------------------------------ using System; +using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; using Dapr.Actors.Client; @@ -175,6 +177,160 @@ namespace Dapr.Actors.Runtime Assert.Equal(1, activator.DeleteCallCount); } + [Fact] + public async Task DeserializeTimer_Period_Iso8601_Time() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"0h0m7s10ms\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Null(result.Ttl); + Assert.Equal(TimeSpan.Zero, result.DueTime); + Assert.Equal(TimeSpan.FromSeconds(7).Add(TimeSpan.FromMilliseconds(10)), result.Period); + } + + [Fact] + public async Task DeserializeTimer_Period_DaprFormat_Every() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"@every 15s\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Null(result.Ttl); + Assert.Equal(TimeSpan.Zero, result.DueTime); + Assert.Equal(TimeSpan.FromSeconds(15), result.Period); + } + + [Fact] + public async Task DeserializeTimer_Period_DaprFormat_Every2() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"@every 3h2m15s\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Null(result.Ttl); + Assert.Equal(TimeSpan.Zero, result.DueTime); + Assert.Equal(TimeSpan.FromHours(3).Add(TimeSpan.FromMinutes(2)).Add(TimeSpan.FromSeconds(15)), result.Period); + } + + [Fact] + public async Task DeserializeTimer_Period_DaprFormat_Monthly() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"@monthly\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Null(result.Ttl); + Assert.Equal(TimeSpan.Zero, result.DueTime); + Assert.Equal(TimeSpan.FromDays(30), result.Period); + } + + [Fact] + public async Task DeserializeTimer_Period_DaprFormat_Weekly() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"@weekly\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Null(result.Ttl); + Assert.Equal(TimeSpan.Zero, result.DueTime); + Assert.Equal(TimeSpan.FromDays(7), result.Period); + } + + [Fact] + public async Task DeserializeTimer_Period_DaprFormat_Daily() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"@daily\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Null(result.Ttl); + Assert.Equal(TimeSpan.Zero, result.DueTime); + Assert.Equal(TimeSpan.FromDays(1), result.Period); + } + + [Fact] + public async Task DeserializeTimer_Period_DaprFormat_Hourly() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"@hourly\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Null(result.Ttl); + Assert.Equal(TimeSpan.Zero, result.DueTime); + Assert.Equal(TimeSpan.FromHours(1), result.Period); + } + + [Fact] + public async Task DeserializeTimer_DueTime_DaprFormat_Hourly() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"dueTime\": \"@hourly\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Null(result.Ttl); + Assert.Equal(TimeSpan.FromHours(1), result.DueTime); + Assert.Equal(TimeSpan.Zero, result.Period); + } + + [Fact] + public async Task DeserializeTimer_DueTime_Iso8601Times() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"dueTime\": \"0h0m7s10ms\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Null(result.Ttl); + Assert.Equal(TimeSpan.Zero, result.Period); + Assert.Equal(TimeSpan.FromSeconds(7).Add(TimeSpan.FromMilliseconds(10)), result.DueTime); + } + + [Fact] + public async Task DeserializeTimer_Ttl_DaprFormat_Hourly() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"ttl\": \"@hourly\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Equal(TimeSpan.Zero, result.DueTime); + Assert.Equal(TimeSpan.Zero, result.Period); + Assert.Equal(TimeSpan.FromHours(1), result.Ttl); + } + + [Fact] + public async Task DeserializeTimer_Ttl_Iso8601Times() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"ttl\": \"0h0m7s10ms\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Equal(TimeSpan.Zero, result.DueTime); + Assert.Equal(TimeSpan.Zero, result.Period); + Assert.Equal(TimeSpan.FromSeconds(7).Add(TimeSpan.FromMilliseconds(10)), result.Ttl); + } + private interface ITestActor : IActor { } private class TestActor : Actor, ITestActor, IDisposable