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

Update client to not dispose user supplied IDisposable types #1954

Merged
merged 19 commits into from May 25, 2021

Conversation

abhipsaMisra
Copy link
Member

@abhipsaMisra abhipsaMisra commented May 11, 2021

This PR fixes two issues:

  • IotHubConnectionString.TokenRefresher is an IDispoable IAuthenticationMethod.AuthenticationWithTokenRefresh, which can be user-supplied. So, we shouldn't dispose it within our client, since they might want to reuse it.
    Reference: https://docs.microsoft.com/en-us/dotnet/api/system.idisposable?view=net-5.0#implementing-idisposable
    "You should implement IDisposable only if your type uses unmanaged resources directly. The consumers of your type can call your IDisposable.Dispose implementation to free resources when the instance is no longer needed."

  • IotHubConnectionString.SharedAccessSignature should be set only if it is non-null and the authentication method of the device client is not of type AuthenticationWithTokenRefresh. Setting the sas value for an AuthenticationWithTokenRefresh authentication type will result in tokens not being renewed. This flow can be hit if the same authentication method is always used to initialize the client; as in, on disposal and reinitialization. (the value of the sas token computed is stored within the authentication method, and on reinitialization the client is incorrectly identified as a fixed-sas-token-initialized client, instead of being identified as a sas-token-refresh-enabled-client.)

Fix for #1911

Dependency code map of IAuthenticationMethod.AuthenticationWithTokenRefresh:

image

Copy link
Contributor

@drwill-ms drwill-ms left a comment

Choose a reason for hiding this comment

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

Breaking change policy

@@ -65,6 +65,10 @@ public abstract class AuthenticationWithTokenRefresh : IAuthenticationMethod, ID
/// </summary>
public bool IsExpiring => (ExpiresOn - DateTime.UtcNow).TotalSeconds <= _bufferSeconds;

// This internal property is used by the sdk to determine if the instance was created by the sdk,
// and thus, if it should be disposed by the sdk.
internal bool ShouldSdkDisposeInstance { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

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

readonly?

@abhipsaMisra abhipsaMisra marked this pull request as draft May 14, 2021 17:25
@abhipsaMisra
Copy link
Member Author

TODO: E2E tests with fault injection?

@@ -21,7 +21,7 @@

using ClientOptions = Microsoft.Azure.Devices.Client.ClientOptions;

namespace Microsoft.Azure.Devices.E2ETests.iothub
namespace Microsoft.Azure.Devices.E2ETests.Iothub.Service
Copy link
Member Author

Choose a reason for hiding this comment

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

minor tangential change - this test was placed under an incorrect namespace

_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;

_amqpIotCbsTokenProvider?.Dispose();
Copy link
Member Author

Choose a reason for hiding this comment

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

the token provider is disposed when the token refresher is disposed

@@ -206,19 +206,17 @@ public async Task<AmqpIotConnection> EnsureConnectionAsync(TimeSpan timeout)
// Create AmqpConnection
amqpIotConnection = await _amqpIotConnector.OpenConnectionAsync(timeout).ConfigureAwait(false);

if (_deviceIdentity.AuthenticationModel != AuthenticationModel.X509)
if (_deviceIdentity.AuthenticationModel == AuthenticationModel.SasGrouped)
Copy link
Member Author

Choose a reason for hiding this comment

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

_deviceIdentity.AuthenticationModel is an enum, so we can simplify this check

Copy link
Member

Choose a reason for hiding this comment

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

What about AuthenticationModel.SasIndividual ? Does it not apply to that?

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 sas individual authenticated devices, the token refresher is associated with the amqp unit: https://github.com/Azure/azure-iot-sdk-csharp/blob/master/iothub/device/src/Transport/Amqp/AmqpUnit.cs#L119

sas individual => amqp unit
sas group => amqp connection
x509 => no token refresher (and also, no multiplexing)

// For device sas authenticated clients the authentication refresher is associated with the AMQP unit itself,
// so it needs to be explicitly disposed.
_amqpAuthenticationRefresher?.StopLoop();
_amqpAuthenticationRefresher?.Dispose();
Copy link
Member Author

Choose a reason for hiding this comment

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

stop and dispose the authentication refresher on disposal of an amqp unit

@@ -9,9 +9,10 @@

namespace Microsoft.Azure.Devices.Client.Transport.AmqpIot
{
internal class AmqpIotCbsTokenProvider : ICbsTokenProvider
internal class AmqpIotCbsTokenProvider : ICbsTokenProvider, IDisposable
Copy link
Member Author

Choose a reason for hiding this comment

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

AmqpIotCbsTokenProvider contains an IDisposable type, so making it IDisposable as well

&& iotHubConnectionString.TokenRefresher != null
&& iotHubConnectionString.TokenRefresher.InstanceCreatedBySdk)
{
iotHubConnectionString.TokenRefresher.Dispose();
Copy link
Member Author

Choose a reason for hiding this comment

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

if applicable, dispose the token refresher when the http client helper is disposed

&& iotHubConnectionString.TokenRefresher != null
&& iotHubConnectionString.TokenRefresher.InstanceCreatedBySdk)
{
iotHubConnectionString.TokenRefresher.Dispose();
Copy link
Member Author

Choose a reason for hiding this comment

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

if applicable, dispose the token refresher when the mqtt iot hub adapter shuts down.
Note that even though we have a token refresher associated with the mqtt connection, it isn't a "true" refresher in the sense that it doesn't monitor token expiration and doesn't initiate a preemptive renewal. The token here is refreshed only on CONNECT (if expired).

@abhipsaMisra abhipsaMisra marked this pull request as ready for review May 18, 2021 16:24
// For this reason, if the device identity of the client is not the one associated with the
// connection, the associated TokenRefresher can be safely disposed.
// Note - This does not cause any identity related issues since the group sas tokens are generated
// against "{IoT hub name}.azure-devices.net/" as the intended audience (without the "device Id").
Copy link
Contributor

Choose a reason for hiding this comment

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

Not if US Gov't cloud, or in the future with vanity domains.

Copy link
Member Author

Choose a reason for hiding this comment

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

In that case, I'll edit to to simply say "hub hostname".

doc/devguide.md Outdated Show resolved Hide resolved
deviceClients[0].Dispose();

amqpConnectionStatuses[0].LastConnectionStatus.Should().Be(ConnectionStatus.Disabled);

Copy link
Member

Choose a reason for hiding this comment

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

Could also add amqpConnectionStatuses[1].LastConnectionStatus.Should().Be(ConnectionStatus.Connected);

Copy link
Member Author

Choose a reason for hiding this comment

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

we actually check the status of the entire list of client instances right below

// connection, the associated TokenRefresher can be safely disposed.
// Note - This does not cause any identity related issues since the group sas tokens are generated
// against the hub host as the intended audience (without the "device Id").
if (deviceIdentity.AuthenticationModel == AuthenticationModel.SasGrouped
Copy link
Member

Choose a reason for hiding this comment

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

Is there no way to avoid creating these for mux scenarios instead of disposing the unnecessary ones? Looks like all we need is a 1SAS token for the connection irrespective of the devices in the mux.

Copy link
Member Author

Choose a reason for hiding this comment

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

There isn't, no. The client is "set up" in a different layer (in internal client, before the whole concept of transport pipeline comes into picture), and this is where the token refresher is set up. This layer doesn't have any visibility about multiplexing, which is a transport-level construct.


var builder = IotHubConnectionStringBuilder.Create(connectionString);
return builder.DeviceId;
}
Copy link
Member

Choose a reason for hiding this comment

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

These tests are doing a lot more than just checking for reusing the disposable created external to the SDK. Is that because we don't have tests that cover these mux scenarios? If they already exist, it would be nice simplify to just testing the reusability.

Copy link
Member Author

Choose a reason for hiding this comment

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

We have tests that cover basic mux and pooling scenarios (all operations), and that cover fault injection when devices have opted for multiplexing (and pooling).
These new tests are designed to reuse the authentication method, while still testing that disposal on one multiplexed device doesn't cause cascading failures in the rest of the devices on the same tcp connection.

Copy link
Member

Choose a reason for hiding this comment

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

This seems more explanatory. Can you just add this in comments?

Copy link
Member Author

Choose a reason for hiding this comment

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

sure, I'll add this comment in

@abhipsaMisra
Copy link
Member Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

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.

None yet

3 participants