Implement redis storage

This commit is contained in:
Maksym Pavlenko
2016-11-08 22:51:03 -08:00
parent d9dd677329
commit 9155af6711
9 changed files with 239 additions and 2 deletions
@@ -0,0 +1,24 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Podsync.Services.Storage;
namespace Podsync.Controllers
{
public class StatusController : Controller
{
private readonly IStorageService _storageService;
public StatusController(IStorageService storageService)
{
_storageService = storageService;
}
public async Task<string> Index()
{
var time = await _storageService.Ping();
return $"Path: {Request.Path}\r\n" +
$"Redis: {time}";
}
}
}
@@ -3,5 +3,7 @@
public class PodsyncConfiguration
{
public string YouTubeApiKey { get; set; }
public string RedisConnectionString { get; set; }
}
}
@@ -0,0 +1,13 @@
using Podsync.Services.Links;
namespace Podsync.Services.Storage
{
public struct FeedMetadata
{
public Provider Provider { get; set; }
public LinkType LinkType { get; set; }
public string Id { get; set; }
}
}
@@ -0,0 +1,14 @@
using System;
using System.Threading.Tasks;
namespace Podsync.Services.Storage
{
public interface IStorageService : IDisposable
{
Task<TimeSpan> Ping();
Task<string> Save(FeedMetadata metadata);
Task<FeedMetadata> Load(string key);
}
}
@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using HashidsNet;
using Microsoft.Extensions.Options;
using Podsync.Services.Links;
using StackExchange.Redis;
namespace Podsync.Services.Storage
{
public class RedisStorage : IStorageService
{
private const string IdKey = "keygen";
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 static readonly IHashids HashIds = new Hashids(IdSalt, IdLength);
private readonly IDatabase _db;
public RedisStorage(IOptions<PodsyncConfiguration> configuration)
{
var cs = configuration.Value.RedisConnectionString;
var connection = ConnectionMultiplexer.ConnectAsync(cs).GetAwaiter().GetResult();
_db = connection.GetDatabase();
}
public void Dispose()
{
_db.Multiplexer.Dispose();
}
public Task<TimeSpan> Ping()
{
return _db.PingAsync();
}
public async Task<string> Save(FeedMetadata metadata)
{
var id = await MakeId();
await _db.HashSetAsync(id, new[]
{
new HashEntry(ProviderField, metadata.Provider.ToString()),
new HashEntry(TypeField, metadata.LinkType.ToString()),
new HashEntry(IdField, metadata.Id)
});
await _db.KeyExpireAsync(id, TimeSpan.FromDays(1));
return id;
}
public async Task<FeedMetadata> Load(string key)
{
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentException("Feed key can't be empty");
}
var entries = await _db.HashGetAllAsync(key);
if (entries.Length == 0)
{
throw new KeyNotFoundException("Invaid key");
}
var metadata = new FeedMetadata
{
Id = entries.Single(x => x.Name == IdField).Value,
LinkType = ToEnum<LinkType>(entries.Single(x => x.Name == TypeField).Value),
Provider = ToEnum<Provider>(entries.Single(x => x.Name == ProviderField).Value)
};
return metadata;
}
public Task ResetCounter()
{
return _db.KeyDeleteAsync(IdKey);
}
public async Task<string> MakeId()
{
var id = await _db.StringIncrementAsync(IdKey);
return HashIds.EncodeLong(id);
}
private static T ToEnum<T>(string key)
{
return (T)Enum.Parse(typeof(T), key, true);
}
}
}
+2
View File
@@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
using Podsync.Services;
using Podsync.Services.Links;
using Podsync.Services.Resolver;
using Podsync.Services.Storage;
using Podsync.Services.Videos.YouTube;
namespace Podsync
@@ -39,6 +40,7 @@ namespace Podsync
services.AddSingleton<ILinkService, LinkService>();
services.AddSingleton<IYouTubeClient, YouTubeClient>();
services.AddSingleton<IResolverService, YtdlWrapper>();
services.AddSingleton<IStorageService, RedisStorage>();
// Add framework services.
services.AddMvc();
+2 -1
View File
@@ -9,6 +9,7 @@
},
"Podsync": {
"YouTubeApiKey": ""
"YouTubeApiKey": "",
"RedisConnectionString": "localhost"
}
}
+4 -1
View File
@@ -1,6 +1,7 @@
{
"dependencies": {
"Google.Apis.YouTube.v3": "1.16.0.582",
"Hashids.net": "1.2.2",
"Microsoft.AspNetCore.Diagnostics": "1.0.0",
"Microsoft.AspNetCore.Mvc": "1.0.0",
"Microsoft.AspNetCore.Razor.Tools": {
@@ -23,12 +24,14 @@
"type": "platform"
},
"Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0",
"StackExchange.Redis": "1.1.608",
"System.Xml.XmlSerializer": "4.0.11"
},
"tools": {
"BundlerMinifier.Core": "2.0.238",
"Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final",
"Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final"
"Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final",
"Microsoft.Extensions.SecretManager.Tools": "1.0.0-preview2-final"
},
"frameworks": {
"netcoreapp1.0": {
@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Podsync.Services.Links;
using Podsync.Services.Storage;
using Xunit;
namespace Podsync.Tests.Services.Storage
{
public class RedisStorageTests : TestBase, IDisposable
{
private readonly RedisStorage _storage;
public RedisStorageTests()
{
_storage = new RedisStorage(Options);
}
[Fact]
public async Task PingTest()
{
var time = await _storage.Ping();
Assert.True(time.TotalMilliseconds > 0);
}
[Fact]
public void MakeIdTest()
{
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();
}
}
[Fact]
public async Task SaveLoadFeedTest()
{
var feed = new FeedMetadata
{
Id = "123",
LinkType = LinkType.Channel,
Provider = Provider.Vimeo
};
var id = await _storage.Save(feed);
Assert.NotEmpty(id);
Assert.Equal(4, id.Length);
var loaded = await _storage.Load(id);
Assert.Equal(feed.Id, loaded.Id);
Assert.Equal(feed.LinkType, loaded.LinkType);
Assert.Equal(feed.Provider, loaded.Provider);
}
[Fact]
public Task LoadInvalidFeedTest()
{
return Assert.ThrowsAsync<KeyNotFoundException>(() => _storage.Load("test"));
}
public void Dispose()
{
_storage.Dispose();
}
}
}