New: Cache goodreads responses

pull/38/head
ta264 5 years ago
parent 50e9225574
commit 3fa605177c

@ -18,6 +18,7 @@ namespace NzbDrone.Common.Test
{ {
var container = MainAppContainerBuilder.BuildContainer(new StartupContext()); var container = MainAppContainerBuilder.BuildContainer(new StartupContext());
container.Register<IMainDatabase>(new MainDatabase(null)); container.Register<IMainDatabase>(new MainDatabase(null));
container.Register<ICacheDatabase>(new CacheDatabase(null));
container.Resolve<IAppFolderFactory>().Register(); container.Resolve<IAppFolderFactory>().Register();
Mocker.SetConstant(container); Mocker.SetConstant(container);

@ -14,6 +14,7 @@ namespace NzbDrone.Common.Extensions
private const string DB = "readarr.db"; private const string DB = "readarr.db";
private const string DB_RESTORE = "readarr.restore"; private const string DB_RESTORE = "readarr.restore";
private const string LOG_DB = "logs.db"; private const string LOG_DB = "logs.db";
private const string CACHE_DB = "cache.db";
private const string NLOG_CONFIG_FILE = "nlog.config"; private const string NLOG_CONFIG_FILE = "nlog.config";
private const string UPDATE_CLIENT_EXE_NAME = "Readarr.Update"; private const string UPDATE_CLIENT_EXE_NAME = "Readarr.Update";
@ -322,6 +323,11 @@ namespace NzbDrone.Common.Extensions
return Path.Combine(GetAppDataPath(appFolderInfo), LOG_DB); return Path.Combine(GetAppDataPath(appFolderInfo), LOG_DB);
} }
public static string GetCacheDatabase(this IAppFolderInfo appFolderInfo)
{
return Path.Combine(GetAppDataPath(appFolderInfo), CACHE_DB);
}
public static string GetNlogConfigPath(this IAppFolderInfo appFolderInfo) public static string GetNlogConfigPath(this IAppFolderInfo appFolderInfo)
{ {
return Path.Combine(appFolderInfo.StartUpFolder, NLOG_CONFIG_FILE); return Path.Combine(appFolderInfo.StartUpFolder, NLOG_CONFIG_FILE);

@ -1,4 +1,5 @@
using System; using System;
using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Common.Cloud; using NzbDrone.Common.Cloud;
@ -28,6 +29,11 @@ namespace NzbDrone.Core.Test.Framework
Mocker.SetConstant<IHttpClient>(new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), Mocker.Resolve<UserAgentBuilder>(), TestLogger)); Mocker.SetConstant<IHttpClient>(new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), Mocker.Resolve<UserAgentBuilder>(), TestLogger));
Mocker.SetConstant<IReadarrCloudRequestBuilder>(new ReadarrCloudRequestBuilder()); Mocker.SetConstant<IReadarrCloudRequestBuilder>(new ReadarrCloudRequestBuilder());
Mocker.SetConstant<IMetadataRequestBuilder>(Mocker.Resolve<MetadataRequestBuilder>()); Mocker.SetConstant<IMetadataRequestBuilder>(Mocker.Resolve<MetadataRequestBuilder>());
var httpClient = Mocker.Resolve<IHttpClient>();
Mocker.GetMock<ICachedHttpResponseService>()
.Setup(x => x.Get(It.IsAny<HttpRequest>(), It.IsAny<TimeSpan>()))
.Returns((HttpRequest request, TimeSpan ttl) => httpClient.Get(request));
} }
} }

@ -0,0 +1,33 @@
using System;
using System.Data;
namespace NzbDrone.Core.Datastore
{
public interface ICacheDatabase : IDatabase
{
}
public class CacheDatabase : ICacheDatabase
{
private readonly IDatabase _database;
public CacheDatabase(IDatabase database)
{
_database = database;
}
public IDbConnection OpenConnection()
{
return _database.OpenConnection();
}
public Version Version => _database.Version;
public int Migration => _database.Migration;
public void Vacuum()
{
_database.Vacuum();
}
}
}

@ -9,6 +9,7 @@ namespace NzbDrone.Core.Datastore
{ {
string MainDbConnectionString { get; } string MainDbConnectionString { get; }
string LogDbConnectionString { get; } string LogDbConnectionString { get; }
string CacheDbConnectionString { get; }
string GetDatabasePath(string connectionString); string GetDatabasePath(string connectionString);
} }
@ -18,10 +19,12 @@ namespace NzbDrone.Core.Datastore
{ {
MainDbConnectionString = GetConnectionString(appFolderInfo.GetDatabase()); MainDbConnectionString = GetConnectionString(appFolderInfo.GetDatabase());
LogDbConnectionString = GetConnectionString(appFolderInfo.GetLogDatabase()); LogDbConnectionString = GetConnectionString(appFolderInfo.GetLogDatabase());
CacheDbConnectionString = GetConnectionString(appFolderInfo.GetCacheDatabase());
} }
public string MainDbConnectionString { get; private set; } public string MainDbConnectionString { get; private set; }
public string LogDbConnectionString { get; private set; } public string LogDbConnectionString { get; private set; }
public string CacheDbConnectionString { get; private set; }
public string GetDatabasePath(string connectionString) public string GetDatabasePath(string connectionString)
{ {

@ -48,6 +48,10 @@ namespace NzbDrone.Core.Datastore
var logDb = new LogDatabase(container.Resolve<IDbFactory>().Create(MigrationType.Log)); var logDb = new LogDatabase(container.Resolve<IDbFactory>().Create(MigrationType.Log));
container.Register<ILogDatabase>(logDb); container.Register<ILogDatabase>(logDb);
var cacheDb = new CacheDatabase(container.Resolve<IDbFactory>().Create(MigrationType.Cache));
container.Register<ICacheDatabase>(cacheDb);
} }
public DbFactory(IMigrationController migrationController, public DbFactory(IMigrationController migrationController,
@ -88,6 +92,14 @@ namespace NzbDrone.Core.Datastore
break; break;
} }
case MigrationType.Cache:
{
connectionString = _connectionStringFactory.CacheDbConnectionString;
CreateLog(connectionString, migrationContext);
break;
}
default: default:
{ {
throw new ArgumentException("Invalid MigrationType"); throw new ArgumentException("Invalid MigrationType");

@ -364,5 +364,15 @@ namespace NzbDrone.Core.Datastore.Migration
.WithColumn("ExceptionType").AsString().Nullable() .WithColumn("ExceptionType").AsString().Nullable()
.WithColumn("Level").AsString(); .WithColumn("Level").AsString();
} }
protected override void CacheDbUpgrade()
{
Create.TableForModel("HttpResponse")
.WithColumn("Url").AsString().Indexed()
.WithColumn("LastRefresh").AsDateTime()
.WithColumn("Expiry").AsDateTime().Indexed()
.WithColumn("Value").AsString()
.WithColumn("StatusCode").AsInt32();
}
} }
} }

@ -3,6 +3,7 @@
public enum MigrationType public enum MigrationType
{ {
Main, Main,
Log Log,
Cache
} }
} }

@ -22,6 +22,10 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
{ {
} }
protected virtual void CacheDbUpgrade()
{
}
public int Version public int Version
{ {
get get
@ -48,6 +52,11 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
_logger.Info("Starting migration to " + Version); _logger.Info("Starting migration to " + Version);
LogDbUpgrade(); LogDbUpgrade();
return; return;
case MigrationType.Cache:
_logger.Info("Starting migration to " + Version);
CacheDbUpgrade();
return;
default: default:
LogDbUpgrade(); LogDbUpgrade();
MainDbUpgrade(); MainDbUpgrade();

@ -14,6 +14,7 @@ using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Extras.Metadata; using NzbDrone.Core.Extras.Metadata;
using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Extras.Others; using NzbDrone.Core.Extras.Others;
using NzbDrone.Core.Http;
using NzbDrone.Core.ImportLists; using NzbDrone.Core.ImportLists;
using NzbDrone.Core.ImportLists.Exclusions; using NzbDrone.Core.ImportLists.Exclusions;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
@ -186,6 +187,8 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity<CustomFilter>("CustomFilters").RegisterModel(); Mapper.Entity<CustomFilter>("CustomFilters").RegisterModel();
Mapper.Entity<ImportListExclusion>("ImportListExclusions").RegisterModel(); Mapper.Entity<ImportListExclusion>("ImportListExclusions").RegisterModel();
Mapper.Entity<CachedHttpResponse>("HttpResponse").RegisterModel();
} }
private static void RegisterMappers() private static void RegisterMappers()

@ -0,0 +1,23 @@
using Dapper;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Housekeeping.Housekeepers
{
public class TrimHttpCache : IHousekeepingTask
{
private readonly ICacheDatabase _database;
public TrimHttpCache(ICacheDatabase database)
{
_database = database;
}
public void Clean()
{
using (var mapper = _database.OpenConnection())
{
mapper.Execute(@"DELETE FROM HttpResponse WHERE Expiry < date('now')");
}
}
}
}

@ -0,0 +1,14 @@
using System;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Http
{
public class CachedHttpResponse : ModelBase
{
public string Url { get; set; }
public DateTime LastRefresh { get; set; }
public DateTime Expiry { get; set; }
public string Value { get; set; }
public int StatusCode { get; set; }
}
}

@ -0,0 +1,27 @@
using System.Linq;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Http
{
public interface ICachedHttpResponseRepository : IBasicRepository<CachedHttpResponse>
{
CachedHttpResponse FindByUrl(string url);
}
public class CachedHttpResponseRepository : BasicRepository<CachedHttpResponse>, ICachedHttpResponseRepository
{
public CachedHttpResponseRepository(ICacheDatabase database,
IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
public CachedHttpResponse FindByUrl(string url)
{
var edition = Query(x => x.Url == url).SingleOrDefault();
return edition;
}
}
}

@ -0,0 +1,58 @@
using System;
using System.Net;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Http
{
public interface ICachedHttpResponseService
{
HttpResponse Get(HttpRequest request, TimeSpan ttl);
}
public class CachedHttpResponseService : ICachedHttpResponseService
{
private readonly ICachedHttpResponseRepository _repo;
private readonly IHttpClient _httpClient;
public CachedHttpResponseService(ICachedHttpResponseRepository httpResponseRepository,
IHttpClient httpClient)
{
_repo = httpResponseRepository;
_httpClient = httpClient;
}
public HttpResponse Get(HttpRequest request, TimeSpan ttl)
{
var cached = _repo.FindByUrl(request.Url.ToString());
if (cached != null && cached.Expiry > DateTime.UtcNow)
{
return new HttpResponse(request, new HttpHeader(), cached.Value, (HttpStatusCode)cached.StatusCode);
}
var result = _httpClient.Get(request);
if (!result.HasHttpError)
{
if (cached == null)
{
cached = new CachedHttpResponse
{
Url = request.Url.ToString(),
};
}
var now = DateTime.UtcNow;
cached.LastRefresh = now;
cached.Expiry = now.Add(ttl);
cached.Value = result.Content;
cached.StatusCode = (int)result.StatusCode;
_repo.Upsert(cached);
}
return result;
}
}
}

@ -10,6 +10,7 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Books; using NzbDrone.Core.Books;
using NzbDrone.Core.Exceptions; using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Http;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
@ -27,6 +28,7 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
RegexOptions.IgnoreCase | RegexOptions.Compiled); RegexOptions.IgnoreCase | RegexOptions.Compiled);
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly ICachedHttpResponseService _cachedHttpClient;
private readonly Logger _logger; private readonly Logger _logger;
private readonly IAuthorService _authorService; private readonly IAuthorService _authorService;
private readonly IBookService _bookService; private readonly IBookService _bookService;
@ -36,6 +38,7 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
private readonly ICached<HashSet<string>> _cache; private readonly ICached<HashSet<string>> _cache;
public GoodreadsProxy(IHttpClient httpClient, public GoodreadsProxy(IHttpClient httpClient,
ICachedHttpResponseService cachedHttpClient,
IAuthorService authorService, IAuthorService authorService,
IBookService bookService, IBookService bookService,
IEditionService editionService, IEditionService editionService,
@ -43,6 +46,7 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
ICacheManager cacheManager) ICacheManager cacheManager)
{ {
_httpClient = httpClient; _httpClient = httpClient;
_cachedHttpClient = cachedHttpClient;
_authorService = authorService; _authorService = authorService;
_bookService = bookService; _bookService = bookService;
_editionService = editionService; _editionService = editionService;
@ -80,7 +84,7 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
httpRequest.AllowAutoRedirect = true; httpRequest.AllowAutoRedirect = true;
httpRequest.SuppressHttpError = true; httpRequest.SuppressHttpError = true;
var httpResponse = _httpClient.Get(httpRequest); var httpResponse = _cachedHttpClient.Get(httpRequest, TimeSpan.FromDays(30));
if (httpResponse.HasHttpError) if (httpResponse.HasHttpError)
{ {
@ -208,7 +212,7 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
httpRequest.AllowAutoRedirect = true; httpRequest.AllowAutoRedirect = true;
httpRequest.SuppressHttpError = true; httpRequest.SuppressHttpError = true;
var httpResponse = _httpClient.Get(httpRequest); var httpResponse = _cachedHttpClient.Get(httpRequest, TimeSpan.FromDays(7));
if (httpResponse.HasHttpError) if (httpResponse.HasHttpError)
{ {
@ -240,7 +244,7 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
httpRequest.AllowAutoRedirect = true; httpRequest.AllowAutoRedirect = true;
httpRequest.SuppressHttpError = true; httpRequest.SuppressHttpError = true;
var httpResponse = _httpClient.Get(httpRequest); var httpResponse = _cachedHttpClient.Get(httpRequest, TimeSpan.FromDays(90));
if (httpResponse.HasHttpError) if (httpResponse.HasHttpError)
{ {
@ -314,7 +318,7 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
httpRequest.AllowAutoRedirect = true; httpRequest.AllowAutoRedirect = true;
httpRequest.SuppressHttpError = true; httpRequest.SuppressHttpError = true;
var httpResponse = _httpClient.Get(httpRequest); var httpResponse = _cachedHttpClient.Get(httpRequest, TimeSpan.FromDays(90));
if (httpResponse.HasHttpError) if (httpResponse.HasHttpError)
{ {

@ -31,6 +31,7 @@ namespace NzbDrone.App.Test
_container = MainAppContainerBuilder.BuildContainer(args); _container = MainAppContainerBuilder.BuildContainer(args);
_container.Register<IMainDatabase>(new MainDatabase(null)); _container.Register<IMainDatabase>(new MainDatabase(null));
_container.Register<ICacheDatabase>(new CacheDatabase(null));
// set up a dummy broadcaster to allow tests to resolve // set up a dummy broadcaster to allow tests to resolve
var mockBroadcaster = new Mock<IBroadcastSignalRMessage>(); var mockBroadcaster = new Mock<IBroadcastSignalRMessage>();

Loading…
Cancel
Save