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

Add convenience method for acknowledging writable property update requests #2157

Merged
merged 10 commits into from
Aug 31, 2021

Conversation

abhipsaMisra
Copy link
Member

@abhipsaMisra abhipsaMisra commented Aug 25, 2021

Fix for #2065 , #2097 , #2100

As highlighted in the above feature requests, constructing the correct IWritablePropertyResponse for writable property update requests was not easily discoverable.
This PR aims to make the flow more convenient.

  • On subscribing to SubscribeToWritablePropertyUpdateRequestsAsync, a client application will receive the writable property update requests as a collection of WritableClientProperty values.
  • WritableClientProperty has a convenience method to "ack" these property update requests.
  • The "ack'ed" response is formatted in the format as required by IoT plug and play.

You can now do:

await deviceClient
    .SubscribeToWritablePropertyUpdateRequestsAsync(
        async (writableProperties, userContext) =>
        {
            var writablePropertyAcks = new ClientPropertyCollection();
            foreach (KeyValuePair<string, object> writableProperty in writableProperties)
            {
                if (writableProperty.Value is WritableClientProperty rootLevelWritableClientProperty)
                {
                    string rootLevelPropertyName = writableProperty.Key;
                    writablePropertyAcks.Add(rootLevelPropertyName, rootLevelWritableClientProperty.AcknowledgeWith(CommonClientResponseCodes.OK));
                }
                else if (writableProperty.Value is IDictionary<string, object> componentProperties)
                {
                    string componentName = writableProperty.Key;
                    foreach (KeyValuePair<string, object> componentProperty in componentProperties)
                    {
                        if (componentProperty.Value is WritableClientProperty componentLevelwritableClientProperty)
                        {
                            string componentLevelPropertyName = componentProperty.Key;
                            writablePropertyAcks.AddComponentProperty(componentName, componentLevelPropertyName, componentLevelwritableClientProperty.AcknowledgeWith(CommonClientResponseCodes.OK));
                        }
                    }
                }
            }

            await deviceClient.UpdateClientPropertiesAsync(writablePropertyAcks).ConfigureAwait(false);
        },
        null,
        cts.Token)
    .ConfigureAwait(false);

Note: In earlier discussions we had contemplated updating:

public Task SubscribeToWritablePropertyUpdateRequestsAsync(Func<ClientPropertyCollection, object, Task> callback, object userContext, CancellationToken cancellationToken = default);

to

public Task RespondToWritablePropertyUpdateRequestsAsync(Func<ClientPropertyCollection, object, Task<ClientPropertyCollection>> callback, object userContext, CancellationToken cancellationToken = default);

However, we decided against doing this because this would associate each writable property request with a response (reported property update) API call. Instead, we are formatting the response that the user can then add to a ClientPropertyCollection and issue the update call themselves.

Comment on lines 405 to 417
bool isWritablePropertyAckPresent = clientProperties.ReportedFromClient.TryGetValue(propName, out IWritablePropertyResponse writablePropertyAck);
isWritablePropertyAckPresent.Should().BeTrue();
// TryGetValue doesn't have nested deserialization, so we'll compare the serialized values
JsonConvert.SerializeObject(writablePropertyAck.Value).Should().Be(JsonConvert.SerializeObject(propValue));

bool isWritablePropertyAckPresentSpecific = clientProperties.ReportedFromClient.TryGetValue(propName, out NewtonsoftJsonWritablePropertyResponse writablePropertyAckNewtonSoft);
isWritablePropertyAckPresentSpecific.Should().BeTrue();
// TryGetValue doesn't have nested deserialization, so we'll compare the serialized values
JsonConvert.SerializeObject(writablePropertyAckNewtonSoft.Value).Should().Be(JsonConvert.SerializeObject(propValue));

bool isWritablePropertyAckPresentAsValue = clientProperties.ReportedFromClient.TryGetValue(propName, out T writablePropertyAckValue);
isWritablePropertyAckPresentAsValue.Should().BeTrue();
writablePropertyAckValue.Should().BeEquivalentTo(propValue);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For writable property update responses reported from the client, you should be able to retrieve the property as:

  • generic IWritablePropertyResponse
  • your implemented IWritablePropertyResponse
  • the .Value part of the IWritablePropertyResponse

Copy link
Member

@jamdavi jamdavi Aug 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing as how people might see WritableClientProperty as a potential value. Do we also want to support that scenario?

The reasoning is, in the SubscribeTo... methods we return a collection of these types and customers might believe this is an acceptable way to address the property.

In the case where they get the full twin doc people might want to compare.

I imagine someone doing this.

var props = client.GetClientProperties();
props.TryGetValue("reportedTemperature", out WritableClientProperty thelastrequest);
props.TryGetValue("reportedTemperature", out double mylastresponse);

if (thelastrequest.Value != mylastresponse)
{
    // 205 is a "Reset Content" code
    mylastrequest.AcknowledgeWith(205, "Values were not in sync");
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a valid concern, esp for the reset scenario that you've described above.
My concern is that since WritableClientProperty is something we create (as opposed to an IWritablePropertyResponse that service mandates the response to be in), I'm not sure if we should be returning that for a GetClientProperties() flow, which is supposed to give you the current state of the client's properties. In my mind, updating the response of Subscribe... flow was ok since that gave you the writable props, in an ack-able format, since you were expected to ack it.
I don't have a strong argument for either case though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking of this a bit more, a writable property is always meant to be ack'ed, no matter how we get it, i.e. through a request notification or via the whole "twin". So I think returning a WritableClientProperty in GetClientProperties() flow makes sense.

@abhipsaMisra
Copy link
Member Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@@ -118,6 +118,42 @@ public async Task Properties_ServiceSetsWritablePropertyMapAndDeviceReceivesEven
.ConfigureAwait(false);
}

[LoggedTestMethod]
public async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAndResponds_Mqtt()
Copy link
Contributor

@drwill-ms drwill-ms Aug 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use a data-driven test method instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are run sequentially, so I wanted to avoid the increase in test run time

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How long do the tests take to run? Is the cost to readability and supportability worth the time gain?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be a substantial increase. I remember the test run time decreased from 3+hours to about 1.5hours when we introduced class-level parallel execution. I'd say restricting parallel execution for each set of tests might bring up the test time closer to 2 hours.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get that it could sometimes dramatically increase the time. I'm more curious about this instance, for these tests.

I think we could have a rule that says, if the test takes longer than 10 seconds to run then split it out, otherwise use data-driven. Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. Yes, we could do that, I don't mind.
Afaik NUnit doesn't have the same limitation (parameterized tests can be executed in parallel), so moving our existing tests to NUnit be a good long term change to have.

@abhipsaMisra
Copy link
Member Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@abhipsaMisra
Copy link
Member Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

/// <returns>A writable property update response that can be reported back to the service.</returns>
public IWritablePropertyResponse AcknowledgeWith(int statusCode, string description = default)
{
return Convention.PayloadSerializer.CreateWritablePropertyResponse(Value, statusCode, Version, description);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Value

This assumes that you're acknowledging the exact value that was sent by the service. What if you want to respond with a value that is slightly off? For exmple, the service sends a writable property request to set the temperature to 72.3 degrees, and the device app returns a WritablePropertyResponse that says "we set it to 72.2 degrees" because the hardware had to round down. (I assume this is a valid scenario, but I'm assuming and you know what that means :))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair. We've included the following doc comment in the ack function which calls out how to achieve this:

/// This writable property update response will contain the property value and version supplied in the writable property update request.
/// If you would like to construct your own writable property update response with custom value and version number, you can
/// create an instance of <see cref="PayloadSerializer.CreateWritablePropertyResponse(object, int, long, string)"/>.
/// See <see href="https://docs.microsoft.com/azure/iot-develop/concepts-convention#writable-properties"/> for more details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants