From ab206fb09d0c83060b5f37c3a2da1f88bf8cb92e Mon Sep 17 00:00:00 2001 From: Maksym Pavlenko Date: Fri, 6 Jan 2017 17:47:42 -0800 Subject: [PATCH] Rework Redis serialization --- src/Podsync/Services/Constants.cs | 6 +- src/Podsync/Services/Storage/FeedMetadata.cs | 11 +- src/Podsync/Services/Storage/RedisStorage.cs | 110 +++++++++++------- .../Services/Storage/RedisStorageTests.cs | 19 ++- 4 files changed, 91 insertions(+), 55 deletions(-) diff --git a/src/Podsync/Services/Constants.cs b/src/Podsync/Services/Constants.cs index 1564536..e5ab7cd 100644 --- a/src/Podsync/Services/Constants.cs +++ b/src/Podsync/Services/Constants.cs @@ -1,7 +1,11 @@ -namespace Podsync.Services +using Podsync.Services.Resolver; + +namespace Podsync.Services { public static class Constants { public const int DefaultPageSize = 50; + + public const ResolveType DefaultFormat = ResolveType.VideoHigh; } } \ No newline at end of file diff --git a/src/Podsync/Services/Storage/FeedMetadata.cs b/src/Podsync/Services/Storage/FeedMetadata.cs index e8f558e..8457cb4 100644 --- a/src/Podsync/Services/Storage/FeedMetadata.cs +++ b/src/Podsync/Services/Storage/FeedMetadata.cs @@ -3,11 +3,11 @@ using Podsync.Services.Resolver; namespace Podsync.Services.Storage { - public struct FeedMetadata + public class FeedMetadata { public Provider Provider { get; set; } - public LinkType LinkType { get; set; } + public LinkType Type { get; set; } public string Id { get; set; } @@ -16,5 +16,12 @@ namespace Podsync.Services.Storage public int PageSize { get; set; } public override string ToString() => $"{Provider} ({LinkType}) {Id}"; + + // Workaround for backward compatibility + public LinkType LinkType + { + get { return Type; } + set { Type = value; } + } } } \ No newline at end of file diff --git a/src/Podsync/Services/Storage/RedisStorage.cs b/src/Podsync/Services/Storage/RedisStorage.cs index e0da5de..60b2f8d 100644 --- a/src/Podsync/Services/Storage/RedisStorage.cs +++ b/src/Podsync/Services/Storage/RedisStorage.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Linq.Expressions; using System.Net; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using HashidsNet; using Microsoft.Extensions.Options; -using Podsync.Services.Links; -using Podsync.Services.Resolver; using StackExchange.Redis; namespace Podsync.Services.Storage @@ -18,16 +19,6 @@ namespace Podsync.Services.Storage private const string IdSalt = "65fce519433f4218aa0cee6394225eea"; private const int IdLength = 4; - // Store all fields manually for backward compatibility with existing implementation - private const string ProviderField = "provider"; - private const string TypeField = "type"; - private const string IdField = "id"; - private const string QualityField = "quality"; - private const string PageSizeField = "pageSize"; - - private const ResolveType DefaultQuality = ResolveType.VideoHigh; - private const int DefaultPageSize = 50; - private static readonly IHashids HashIds = new Hashids(IdSalt, IdLength); private readonly string _cs; @@ -85,13 +76,21 @@ namespace Podsync.Services.Storage { var id = await MakeId(); + if (await Db.KeyExistsAsync(id)) + { + throw new InvalidOperationException("Failed to generate feed id"); + } + await Db.HashSetAsync(id, new[] { - new HashEntry(ProviderField, metadata.Provider.ToString()), - new HashEntry(TypeField, metadata.LinkType.ToString()), - new HashEntry(IdField, metadata.Id), - new HashEntry(QualityField, metadata.Quality.ToString()), - new HashEntry(PageSizeField, metadata.PageSize), + // V1 + new HashEntry(nameof(metadata.Provider), metadata.Provider.ToString()), + new HashEntry(nameof(metadata.Type), metadata.Type.ToString()), + new HashEntry(nameof(metadata.Id), metadata.Id), + + // V2 + new HashEntry(nameof(metadata.Quality), metadata.Quality.ToString()), + new HashEntry(nameof(metadata.PageSize), metadata.PageSize), }); await Db.KeyExpireAsync(id, TimeSpan.FromDays(1)); @@ -116,42 +115,71 @@ namespace Podsync.Services.Storage throw new KeyNotFoundException("Invaid key"); } - var metadata = new FeedMetadata - { - Id = entries.Single(x => x.Name == IdField).Value, - LinkType = ToEnum(entries.Single(x => x.Name == TypeField)), - Provider = ToEnum(entries.Single(x => x.Name == ProviderField)), - }; + var metadata = new FeedMetadata(); - if (entries.Length > 3) - { - metadata.Quality = ToEnum(entries.Single(x => x.Name == QualityField)); - metadata.PageSize = (int)entries.Single(x => x.Name == PageSizeField).Value; - } - else - { - // Set default values - metadata.Quality = DefaultQuality; - metadata.PageSize = DefaultPageSize; - } + // V1 + SetProperty(metadata, x => x.Id, entries); + SetProperty(metadata, x => x.Type, entries); + SetProperty(metadata, x => x.Provider, entries); + + // V2 + SetProperty(metadata, x => x.Quality, entries, Constants.DefaultFormat); + SetProperty(metadata, x => x.PageSize, entries, Constants.DefaultPageSize); return metadata; } - public Task ResetCounter() - { - return Db.KeyDeleteAsync(IdKey); - } - public async Task MakeId() { var id = await Db.StringIncrementAsync(IdKey); return HashIds.EncodeLong(id); } - private static T ToEnum(HashEntry key) + private static void SetProperty(T target, Expression> memberLamda, HashEntry[] entries) { - return (T)Enum.Parse(typeof(T), key.Value, true); + SetProperty(target, memberLamda, entries, default(P), true); + } + + private static void SetProperty(T target, Expression> memberLamda, HashEntry[] entries, P fallback, bool throwIfMissing = false) + { + var memberExpression = memberLamda.Body as MemberExpression; + + // Get property name via reflection + var entryName = memberExpression?.Member?.Name; + if (string.IsNullOrEmpty(entryName)) + { + throw new InvalidOperationException("Wrong property expression"); + } + + P value; + + // RedisValue is value type + if (entries.Any(x => string.Equals(x.Name, entryName, StringComparison.OrdinalIgnoreCase))) + { + var entry = entries.Single(x => string.Equals(x.Name, entryName, StringComparison.OrdinalIgnoreCase)); + + var propertyType = typeof(P); + if (propertyType.GetTypeInfo().IsEnum) + { + value = (P)Enum.Parse(propertyType, entry.Value); + } + else + { + value = (P)Convert.ChangeType(entry.Value, propertyType); + } + } + else + { + if (throwIfMissing) + { + throw new InvalidDataException("Missing mandatory property"); + } + + value = fallback; + } + + var property = memberExpression.Member as PropertyInfo; + property?.SetValue(target, value); } } } \ No newline at end of file diff --git a/test/Podsync.Tests/Services/Storage/RedisStorageTests.cs b/test/Podsync.Tests/Services/Storage/RedisStorageTests.cs index 6e0c799..3c78ef5 100644 --- a/test/Podsync.Tests/Services/Storage/RedisStorageTests.cs +++ b/test/Podsync.Tests/Services/Storage/RedisStorageTests.cs @@ -29,17 +29,11 @@ namespace Podsync.Tests.Services.Storage { const int idCount = 50; - try - { - var results = new string[idCount]; - Parallel.For(0, results.Length, (i, _) => results[i] = _storage.MakeId().GetAwaiter().GetResult()); - Assert.Equal(results.Length, results.Distinct().Count()); - } - finally - { - _storage.ResetCounter(); - } + var results = new string[idCount]; + Parallel.For(0, results.Length, (i, _) => results[i] = _storage.MakeId().GetAwaiter().GetResult()); + + Assert.Equal(results.Length, results.Distinct().Count()); } [Fact] @@ -49,7 +43,9 @@ namespace Podsync.Tests.Services.Storage { Id = "123", LinkType = LinkType.Channel, - Provider = Provider.Vimeo + Provider = Provider.Vimeo, + + PageSize = 45 }; var id = await _storage.Save(feed); @@ -61,6 +57,7 @@ namespace Podsync.Tests.Services.Storage Assert.Equal(feed.Id, loaded.Id); Assert.Equal(feed.LinkType, loaded.LinkType); Assert.Equal(feed.Provider, loaded.Provider); + Assert.Equal(45, loaded.PageSize); } [Fact]