question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

GetStateAsync/GetStateEntryAsync works when using redis but fails with SQL Server

See original GitHub issue

I have a small DAPR-enabled service which I originally wrote against the default redis state store. With that, the code worked fine. When I switched the state store to SQL Server, reading from the store started throwing exceptions.

Code:

[HttpPost]
[Route("register")]
public async Task<IActionResult> Register([FromBody] RegistrationRequest model,
                                            [FromServices] DaprClient dapr,
                                            CancellationToken cancellationToken)
{
    var profile = new UserProfile { Name = model.Username, PhoneNumber = model.Phone };

    await dapr.SaveStateAsync(Storage.SqlName, "test", profile, cancellationToken: cancellationToken)
                .ConfigureAwait(false);

    return Ok();
}

[HttpPost]
[Route("login")]
public async Task<LoginResponse> Login([FromBody] LoginRequest model,
                                        [FromServices] DaprClient dapr,
                                        CancellationToken cancellationToken)
{
    var profile = await dapr.GetStateEntryAsync<UserProfile>(Storage.SqlName,
                                                                "test",
                                                                cancellationToken: cancellationToken)
                            .ConfigureAwait(false);

    if (profile.Value is null)
    {
        return new LoginResponse { IsSuccess = false };
    }

    return new LoginResponse
    {
        IsSuccess = true,
        Roles = new[] { "role_1", "role_x" },
        Username = model.Username
    };
}

Expected Behavior

Calling the Login method should succeed.

Actual Behavior

Calling the Register method succeeds and persists the data to the SQL database: image

Subsequently calling the Login method throws an exception in GetStateEntryAsync:

System.Text.Json.JsonException: The JSON value could not be converted to DotNetBa.Dapr.Common.Models.UserProfile. Path: $ | LineNumber: 0 | BytePositionInLine: 58.
   at System.Text.Json.ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Type propertyType)
   at System.Text.Json.JsonPropertyInfoNotNullable`4.OnRead(ReadStack& state, Utf8JsonReader& reader)
   at System.Text.Json.JsonPropertyInfo.Read(JsonTokenType tokenType, ReadStack& state, Utf8JsonReader& reader)
   at System.Text.Json.JsonSerializer.ReadCore(JsonSerializerOptions options, Utf8JsonReader& reader, ReadStack& readStack)
   at System.Text.Json.JsonSerializer.ReadCore(Type returnType, JsonSerializerOptions options, Utf8JsonReader& reader)
   at System.Text.Json.JsonSerializer.Deserialize(String json, Type returnType, JsonSerializerOptions options)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)
   at Dapr.Client.DaprClientGrpc.GetStateAndETagAsync[TValue](String storeName, String key, Nullable`1 consistencyMode, CancellationToken cancellationToken)
   at Dapr.Client.DaprClient.GetStateEntryAsync[TValue](String storeName, String key, Nullable`1 consistencyMode, CancellationToken cancellationToken)
   at DotNetBa.Dapr.UserService.Controllers.LoginController.Login(LoginRequest model, DaprClient dapr, CancellationToken cancellationToken) in C:\Source\Personal\dotnetba-dapr\src\DotNetBa.Dapr.UserService\Controllers\LoginController.cs:line 27
   at lambda_method(Closure , Object )
   at Microsoft.Extensions.Internal.ObjectMethodExecutorAwaitable.Awaiter.GetResult()
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

If I swap the target store for redis, the same code works as expected.

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:1
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

1reaction
mareklinkacommented, May 4, 2020

Well, I cloned the repo and debugged into the DaprClientGrpc to see what’s happening there and it all looks good. The ConvertToAnyAsync works as I would expect and for my test data, produces a ByteString of length 40: image

However, when I get the value back from SQL Server, I get back a length of 58: image

The actual value I get back is a JSON string containing the original 40 byte payload encoded into base64: image

So that seems to indicate that the DAPR SQL component is doing something weird when called by the SDK (and ONLY when called by the SDK - calling the component directly works normally when executed via HTTP, as shown above). Then I realized the SDK is invoking the DAPR API via gRPC so that test above is not representative.

That would seem to indicate that the DAPR SQL component handles HTTP invocation differently than gRPC invocation. So I went and looked into the component source: https://github.com/dapr/components-contrib/blob/b647397b2c81356b62170e4161aa2393fc7ccf03/state/sqlserver/sqlserver.go#L479

That looks suspicious to me (but I’m no Go coder). But that line looks like the component is converting Value into JSON. From the docs:

Marshal returns the JSON encoding of v.

Marshal traverses the value v recursively. If an encountered value implements the Marshaler
interface and is not a nil pointer, Marshal calls its MarshalJSON method to produce JSON. If no
MarshalJSON method is present but the value implements encoding.TextMarshaler instead,
Marshal calls its MarshalText method and encodes the result as a JSON string.

And I think this is how the payload is getting turned into a JSON string in the database. It would also explain why it works correctly when I invoke the component via HTTP - the Value there gets serialized into JSON properly in that case.

At this point I think this whole ticket belongs in the dapr/components-contrib repo 😕

0reactions
LMWFcommented, May 15, 2020

This was moved to dapr/components-contrib.

Read more comments on GitHub >

github_iconTop Results From Across the Web

It was not possible to connect to the redis server(s)
The error you are getting is usually a sign that you have not set abortConnect=false in your connection string. The default value for ......
Read more >
What if Redis Stops Working - How Do I Keep My App ...
An interesting question came up on the CacheManager repository in issue 146. What do I do, if the Redis server dies and might...
Read more >
RedisConnectionException: No connection is available to ...
Connecting to it fails immediately despite the timeouts I've set. When I tried using an invalid host name and/or invalid port the timeouts...
Read more >
Could not connect to redis connection refused - Fix it easily
The most common reason for the connection refused error is that the Redis-Server is not started. Redis server should be started to use...
Read more >
Sitefinity fails to initialize if Redis connection string is invalid
If an incorrect Redis connection string is put, the project stays on the spinning cogs and 'initialising'. It never errors and changes, ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found