Skip to content

Commit

Permalink
Restore device scopes from older store (version < 1.2) (Azure#4889)
Browse files Browse the repository at this point in the history
Edgehub version 1.1 didn't have DeviceScope and ParentScopes in storage and updating to 1.2 fails to start edgeHub because reading from cache fails. 
Fix is to check the service identity that was read from storage for current device (from actorDeviceId) and if the DeviceScope is empty that means it is a restore from older version (edge devices should always have DeviceScope) and set the value for DeviceScope from device details (deviceId and generationId). For leaf devices it sets DeviceScope and ParentScopes to the edge device scope because if the device was in store it must be a child of the edge device and sync from cloud will update the leaf device in case it was removed as child.

This fixes Azure#4828
  • Loading branch information
ancaantochi authored May 12, 2021
1 parent c7d7e58 commit 207a5f0
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public sealed class DeviceScopeIdentitiesCache : IDeviceScopeIdentitiesCache

readonly string edgeDeviceId;
readonly IServiceProxy serviceProxy;
readonly IKeyValueStore<string, string> encryptedStore;
readonly ServiceIdentityStore store;
readonly IServiceIdentityHierarchy serviceIdentityHierarchy;
readonly Timer refreshCacheTimer;
readonly TimeSpan periodicRefreshRate;
Expand All @@ -39,7 +39,7 @@ public sealed class DeviceScopeIdentitiesCache : IDeviceScopeIdentitiesCache
DeviceScopeIdentitiesCache(
IServiceIdentityHierarchy serviceIdentityHierarchy,
IServiceProxy serviceProxy,
IKeyValueStore<string, string> encryptedStorage,
ServiceIdentityStore identityStore,
TimeSpan periodicRefreshRate,
TimeSpan refreshDelay,
TimeSpan initializationRefreshDelay,
Expand All @@ -48,7 +48,7 @@ public sealed class DeviceScopeIdentitiesCache : IDeviceScopeIdentitiesCache
this.serviceIdentityHierarchy = serviceIdentityHierarchy;
this.edgeDeviceId = serviceIdentityHierarchy.GetActorDeviceId();
this.serviceProxy = serviceProxy;
this.encryptedStore = encryptedStorage;
this.store = identityStore;
this.periodicRefreshRate = periodicRefreshRate;
this.refreshDelay = refreshDelay;
this.initializationRefreshDelay = initializationRefreshDelay;
Expand Down Expand Up @@ -88,15 +88,17 @@ public static async Task<DeviceScopeIdentitiesCache> Create(
Preconditions.CheckNotNull(serviceProxy, nameof(serviceProxy));
Preconditions.CheckNotNull(encryptedStorage, nameof(encryptedStorage));
Preconditions.CheckNotNull(serviceIdentityHierarchy, nameof(serviceIdentityHierarchy));
IDictionary<string, StoredServiceIdentity> cache = await ReadCacheFromStore(encryptedStorage);

var identityStore = new ServiceIdentityStore(encryptedStorage);
IDictionary<string, StoredServiceIdentity> cache = await identityStore.ReadCacheFromStore(encryptedStorage, serviceIdentityHierarchy.GetActorDeviceId());

// Populate the serviceIdentityHierarchy
foreach (KeyValuePair<string, StoredServiceIdentity> kvp in cache)
{
await kvp.Value.ServiceIdentity.ForEachAsync(serviceIdentity => serviceIdentityHierarchy.AddOrUpdate(serviceIdentity));
}

var deviceScopeIdentitiesCache = new DeviceScopeIdentitiesCache(serviceIdentityHierarchy, serviceProxy, encryptedStorage, refreshRate, refreshDelay, initializationRefreshDelay, cache.Count > 0);
var deviceScopeIdentitiesCache = new DeviceScopeIdentitiesCache(serviceIdentityHierarchy, serviceProxy, identityStore, refreshRate, refreshDelay, initializationRefreshDelay, cache.Count > 0);

Events.Created();
return deviceScopeIdentitiesCache;
Expand Down Expand Up @@ -292,7 +294,7 @@ public Task<IList<ServiceIdentity>> GetDevicesAndModulesInTargetScopeAsync(strin

public void Dispose()
{
this.encryptedStore?.Dispose();
this.store?.Dispose();
this.refreshCacheTimer?.Dispose();
this.refreshCacheTask?.Dispose();
}
Expand All @@ -311,19 +313,6 @@ internal Task<Option<ServiceIdentity>> GetServiceIdentityFromService(string targ
}
}

static async Task<IDictionary<string, StoredServiceIdentity>> ReadCacheFromStore(IKeyValueStore<string, string> encryptedStore)
{
IDictionary<string, StoredServiceIdentity> cache = new Dictionary<string, StoredServiceIdentity>();
await encryptedStore.IterateBatch(
int.MaxValue,
(key, value) =>
{
cache.Add(key, JsonConvert.DeserializeObject<StoredServiceIdentity>(value));
return Task.CompletedTask;
});
return cache;
}

async Task<bool> ShouldRefreshIdentity(string id)
{
if (!this.isInitialized.Get())
Expand Down Expand Up @@ -460,7 +449,7 @@ async Task HandleNoServiceIdentity(string id)

// Remove the target identity
await this.serviceIdentityHierarchy.Remove(id);
await this.encryptedStore.Remove(id);
await this.store.Remove(id);
Events.NotInScope(id);

if (hasValidServiceIdentity)
Expand All @@ -478,7 +467,7 @@ async Task HandleNewServiceIdentity(ServiceIdentity serviceIdentity)
if (hasChanged)
{
Events.AddInScope(serviceIdentity.Id);
await this.SaveServiceIdentityToStore(serviceIdentity.Id, new StoredServiceIdentity(serviceIdentity));
await this.store.Save(serviceIdentity.Id, new StoredServiceIdentity(serviceIdentity));

if (existing.HasValue)
{
Expand All @@ -487,10 +476,77 @@ async Task HandleNewServiceIdentity(ServiceIdentity serviceIdentity)
}
}

async Task SaveServiceIdentityToStore(string id, StoredServiceIdentity storedServiceIdentity)
internal class ServiceIdentityStore : IDisposable
{
string serviceIdentityString = JsonConvert.SerializeObject(storedServiceIdentity);
await this.encryptedStore.Put(id, serviceIdentityString);
static readonly string DeviceScopeFormat = "ms-azure-iot-edge://{0}-{1}";
readonly IKeyValueStore<string, string> entityStore;

public ServiceIdentityStore(IKeyValueStore<string, string> entityStore)
{
this.entityStore = entityStore;
}

public async Task Save(string id, StoredServiceIdentity storedServiceIdentity)
{
string serviceIdentityString = JsonConvert.SerializeObject(storedServiceIdentity);
await this.entityStore.Put(id, serviceIdentityString);
}

public Task Remove(string id) => this.entityStore.Remove(id);

public async Task<IDictionary<string, StoredServiceIdentity>> ReadCacheFromStore(IKeyValueStore<string, string> encryptedStore, string actorDeviceId)
{
IDictionary<string, StoredServiceIdentity> cache = new Dictionary<string, StoredServiceIdentity>();
await encryptedStore.IterateBatch(
int.MaxValue,
(key, value) =>
{
var storedIdentity = JsonConvert.DeserializeObject<StoredServiceIdentity>(value);
cache.Add(key, storedIdentity);
return Task.CompletedTask;
});

await this.RestoreDeviceScope(encryptedStore, actorDeviceId, cache);

return cache;
}

public void Dispose() => this.entityStore?.Dispose();

// Version 1.1 didn't have deviceScope in store, this method sets the deviceScope for edge device from deviceId and generationid
// for leaf devices set the DeviceScope and ParentScopes to the edge device scope because if they are present in store they must be children of the edge device
async Task RestoreDeviceScope(IKeyValueStore<string, string> encryptedStore, string actorDeviceId, IDictionary<string, StoredServiceIdentity> cache)
{
if (cache.TryGetValue(actorDeviceId, out StoredServiceIdentity storedServiceIdentity))
{
string edgeDeviceScope = null;
storedServiceIdentity.ServiceIdentity.ForEach(si => edgeDeviceScope = si.DeviceScope.OrDefault());

if (string.IsNullOrEmpty(edgeDeviceScope) && storedServiceIdentity.ServiceIdentity.HasValue)
{
var edgeServiceIdentity = storedServiceIdentity.ServiceIdentity.OrDefault();
edgeDeviceScope = string.Format(DeviceScopeFormat, edgeServiceIdentity.DeviceId, edgeServiceIdentity.GenerationId);
List<string> keys = new List<string>(cache.Keys);
foreach (var key in keys)
{
if (key.Equals(actorDeviceId))
{
cache[key] = new StoredServiceIdentity(new ServiceIdentity(edgeServiceIdentity.DeviceId, edgeServiceIdentity.ModuleId.OrDefault(), edgeDeviceScope, edgeServiceIdentity.ParentScopes, edgeServiceIdentity.GenerationId, edgeServiceIdentity.Capabilities, edgeServiceIdentity.Authentication, edgeServiceIdentity.Status));
}
else
{
cache[key].ServiceIdentity.Filter(si => !si.IsEdgeDevice && !si.IsModule && !si.DeviceScope.HasValue && si.ParentScopes.Count == 0).ForEach(si =>
{
cache[key] = new StoredServiceIdentity(new ServiceIdentity(si.DeviceId, si.ModuleId.OrDefault(), edgeDeviceScope, new List<string> { edgeDeviceScope }, si.GenerationId, si.Capabilities, si.Authentication, si.Status));
});
}

string serviceIdentityString = JsonConvert.SerializeObject(cache[key]);
await encryptedStore.Put(key, serviceIdentityString);
}
}
}
}
}

internal class StoredServiceIdentity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,15 @@ public ServiceIdentity(
this.DeviceId = Preconditions.CheckNonWhiteSpace(deviceId, nameof(deviceId));
this.ModuleId = Option.Maybe(moduleId);
this.DeviceScope = Option.Maybe(deviceScope);
this.ParentScopes = new List<string>(Preconditions.CheckNotNull(parentScopes, nameof(parentScopes)));
this.Capabilities = Preconditions.CheckNotNull(capabilities, nameof(capabilities));
this.Authentication = Preconditions.CheckNotNull(authentication, nameof(authentication));
this.Id = this.ModuleId.Map(m => $"{deviceId}/{moduleId}").GetOrElse(deviceId);
this.GenerationId = Preconditions.CheckNonWhiteSpace(generationId, nameof(generationId));
this.Status = status;

this.ParentScopes = parentScopes != null
? new List<string>(parentScopes)
: !this.IsEdgeDevice && !string.IsNullOrWhiteSpace(deviceScope) ? new List<string> { deviceScope } : new List<string>();
}

[JsonIgnore]
Expand Down
Loading

0 comments on commit 207a5f0

Please sign in to comment.