BaseWorker, BaseWorkerWithContext DoWork, call: Scope setting

TrangaBaseContext Sync return with success state and exception message
This commit is contained in:
2025-07-02 22:15:34 +02:00
parent 6cd836540a
commit e327e93163
19 changed files with 125 additions and 99 deletions

View File

@ -63,8 +63,8 @@ public class FileLibraryController(IServiceScope scope) : Controller
//TODO Path check
library.BasePath = newBasePath;
if(context.Sync().Result is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
if(context.Sync().Result is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}
@ -90,8 +90,8 @@ public class FileLibraryController(IServiceScope scope) : Controller
//TODO Name check
library.LibraryName = newName;
if(context.Sync().Result is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
if(context.Sync().Result is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}
@ -111,8 +111,8 @@ public class FileLibraryController(IServiceScope scope) : Controller
//TODO Parameter check
context.FileLibraries.Add(library);
if(context.Sync().Result is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
if(context.Sync().Result is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Created();
}
@ -134,8 +134,8 @@ public class FileLibraryController(IServiceScope scope) : Controller
context.FileLibraries.Remove(library);
if(context.Sync().Result is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
if(context.Sync().Result is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}
}

View File

@ -60,8 +60,8 @@ public class LibraryConnectorController(IServiceScope scope) : Controller
context.LibraryConnectors.Add(libraryConnector);
if(context.Sync().Result is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
if(context.Sync().Result is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Created();
}
@ -84,8 +84,8 @@ public class LibraryConnectorController(IServiceScope scope) : Controller
context.LibraryConnectors.Remove(connector);
if(context.Sync().Result is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
if(context.Sync().Result is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}
}

View File

@ -88,8 +88,8 @@ public class MangaConnectorController(IServiceScope scope) : Controller
connector.Enabled = Enabled;
if(context.Sync().Result is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
if(context.Sync().Result is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Accepted();
}
}

View File

@ -81,8 +81,8 @@ public class MangaController(IServiceScope scope) : Controller
context.Mangas.Remove(manga);
if(context.Sync().Result is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
if(context.Sync().Result is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}
@ -329,8 +329,8 @@ public class MangaController(IServiceScope scope) : Controller
return NotFound();
manga.IgnoreChaptersBefore = chapterThreshold;
if(context.Sync().Result is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
if(context.Sync().Result is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Accepted();
}
@ -354,7 +354,7 @@ public class MangaController(IServiceScope scope) : Controller
return NotFound(nameof(LibraryId));
MoveMangaLibraryWorker moveLibrary = new(manga, library, scope);
UpdateChaptersDownloadedWorker updateDownloadedFiles = new(manga, scope, [moveLibrary]);
UpdateChaptersDownloadedWorker updateDownloadedFiles = new(manga, [moveLibrary]);
Tranga.AddWorkers([moveLibrary, updateDownloadedFiles]);

View File

@ -119,8 +119,8 @@ public class MetadataFetcherController(IServiceScope scope) : Controller
context.Remove(entry);
if(context.Sync().Result is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
if(context.Sync().Result is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Ok();
}
}

View File

@ -62,8 +62,8 @@ public class NotificationConnectorController(IServiceScope scope) : Controller
context.NotificationConnectors.Add(notificationConnector);
if(context.Sync().Result is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
if(context.Sync().Result is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Created();
}
@ -156,8 +156,8 @@ public class NotificationConnectorController(IServiceScope scope) : Controller
context.NotificationConnectors.Remove(connector);
if(context.Sync().Result is { } errorMessage)
return StatusCode(Status500InternalServerError, errorMessage);
if(context.Sync().Result is { success: false } result)
return StatusCode(Status500InternalServerError, result.exceptionMessage);
return Created();
}
}

View File

@ -96,7 +96,7 @@ public class SearchController(IServiceScope scope) : Controller
if(context.MangaConnectorToManga.Find(addMcId.Key) is null)
context.MangaConnectorToManga.Add(mcId);
if (context.Sync().Result is not null)
if (context.Sync().Result is { success: false } )
return null;
return manga;
}

View File

@ -1,10 +1,8 @@
using API.APIEndpointRecords;
using API.Schema.MangaContext;
using API.Workers;
using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
@ -129,7 +127,7 @@ public class WorkerController(ILog Log) : Controller
if (worker.State >= WorkerExecutionState.Waiting)
return StatusCode(Status412PreconditionFailed, "Already running");
Tranga.StartWorker(worker);
Tranga.MarkWorkerForStart(worker);
return Ok();
}

View File

@ -22,17 +22,17 @@ public abstract class TrangaBaseContext<T> : DbContext where T : DbContext
}, Array.Empty<string>(), LogLevel.Warning, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category | DbContextLoggerOptions.UtcTime);
}
internal async Task<string?> Sync()
internal async Task<(bool success, string? exceptionMessage)> Sync()
{
try
{
await this.SaveChangesAsync();
return null;
return (true, null);
}
catch (Exception e)
{
Log.Error(null, e);
return e.Message;
return (false, e.Message);
}
}
}

View File

@ -2,6 +2,7 @@
using API.Workers;
using log4net;
using log4net.Config;
using Microsoft.EntityFrameworkCore;
namespace API;
@ -54,6 +55,7 @@ public static class Tranga
private static readonly Dictionary<BaseWorker, Thread> RunningWorkers = new();
public static BaseWorker[] GetRunningWorkers() => RunningWorkers.Keys.ToArray();
private static readonly HashSet<BaseWorker> StartWorkers = new();
private static void WorkerStarter(object? serviceProviderObj)
{
Log.Info("WorkerStarter Thread running.");
@ -66,19 +68,26 @@ public static class Tranga
while (true)
{
using IServiceScope scope = serviceProvider.CreateScope();
foreach (BaseWorker startWorker in StartWorkers)
{
IServiceScope scope = serviceProvider.CreateScope();
StartWorker(startWorker, scope);
}
Thread.Sleep(TrangaSettings.workCycleTimeout);
}
}
internal static void StartWorker(BaseWorker worker)
internal static void MarkWorkerForStart(BaseWorker worker) => StartWorkers.Add(worker);
private static void StartWorker(BaseWorker worker, IServiceScope scope)
{
throw new NotImplementedException();
if(worker is BaseWorkerWithContext<DbContext> w)
w.SetScope(scope);
worker.DoWork();
}
internal static void StopWorker(BaseWorker worker)
{
throw new NotImplementedException();
worker.Cancel();
}
}

View File

@ -5,21 +5,44 @@ namespace API.Workers;
public abstract class BaseWorker : Identifiable
{
/// <summary>
/// Workers this Worker depends on being completed before running.
/// </summary>
public BaseWorker[] DependsOn { get; init; }
/// <summary>
/// Dependencies and dependencies of dependencies. See also <see cref="DependsOn"/>.
/// </summary>
public IEnumerable<BaseWorker> AllDependencies => DependsOn.Select(d => d.AllDependencies).SelectMany(x => x);
/// <summary>
/// <see cref="AllDependencies"/> and Self.
/// </summary>
public IEnumerable<BaseWorker> DependenciesAndSelf => AllDependencies.Append(this);
/// <summary>
/// <see cref="DependsOn"/> where <see cref="WorkerExecutionState"/> is less than Completed.
/// </summary>
public IEnumerable<BaseWorker> MissingDependencies => DependsOn.Where(d => d.State < WorkerExecutionState.Completed);
public bool DependenciesFulfilled => DependsOn.All(d => d.State >= WorkerExecutionState.Completed);
internal WorkerExecutionState State { get; set; }
public bool AllDependenciesFulfilled => DependsOn.All(d => d.State >= WorkerExecutionState.Completed);
internal WorkerExecutionState State { get; private set; }
private static readonly CancellationTokenSource CancellationTokenSource = new(TimeSpan.FromMinutes(10));
protected ILog Log { get; init; }
/// <summary>
/// Stops worker, and marks as <see cref="WorkerExecutionState"/>.Cancelled
/// </summary>
public void Cancel()
{
this.State = WorkerExecutionState.Cancelled;
CancellationTokenSource.Cancel();
}
protected void Fail() => this.State = WorkerExecutionState.Failed;
/// <summary>
/// Stops worker, and marks as <see cref="WorkerExecutionState"/>.Failed
/// </summary>
protected void Fail()
{
this.State = WorkerExecutionState.Failed;
CancellationTokenSource.Cancel();
}
public BaseWorker(IEnumerable<BaseWorker>? dependsOn = null)
{
@ -27,6 +50,22 @@ public abstract class BaseWorker : Identifiable
this.Log = LogManager.GetLogger(GetType());
}
/// <summary>
/// Sets States during worker-run.
/// States:
/// <list type="bullet">
/// <item><see cref="WorkerExecutionState"/>.Waiting when waiting for <see cref="MissingDependencies"/></item>
/// <item><see cref="WorkerExecutionState"/>.Running when running</item>
/// <item><see cref="WorkerExecutionState"/>.Completed after finished</item>
/// </list>
/// </summary>
/// <returns>
/// <list type="bullet">
/// <item>If <see cref="BaseWorker"/> has <see cref="MissingDependencies"/>, missing dependencies.</item>
/// <item>If <see cref="MissingDependencies"/> are <see cref="WorkerExecutionState"/>.Running, itself after waiting for dependencies.</item>
/// <item>If <see cref="BaseWorker"/> has run, additional <see cref="BaseWorker"/>.</item>
/// </list>
/// </returns>
public Task<BaseWorker[]> DoWork()
{
this.State = WorkerExecutionState.Waiting;
@ -60,9 +99,9 @@ public abstract class BaseWorker : Identifiable
public enum WorkerExecutionState
{
Failed = 0,
Cancelled = 32,
Created = 64,
Waiting = 96,
Running = 128,
Completed = 192,
Cancelled = 193
Completed = 192
}

View File

@ -1,8 +1,18 @@
using System.Configuration;
using Microsoft.EntityFrameworkCore;
namespace API.Workers;
public abstract class BaseWorkerWithContext<T>(IServiceScope scope, IEnumerable<BaseWorker>? dependsOn = null) : BaseWorker(dependsOn) where T : DbContext
public abstract class BaseWorkerWithContext<T>(IEnumerable<BaseWorker>? dependsOn = null) : BaseWorker(dependsOn) where T : DbContext
{
protected T DbContext { get; init; } = scope.ServiceProvider.GetRequiredService<T>();
protected T? DbContext = null;
public void SetScope(IServiceScope scope) => DbContext = scope.ServiceProvider.GetRequiredService<T>();
/// <exception cref="ConfigurationErrorsException">Scope has not been set. <see cref="SetScope"/></exception>
public new Task<BaseWorker[]> DoWork()
{
if (DbContext is null)
throw new ConfigurationErrorsException("Scope has not been set.");
return base.DoWork();
}
}

View File

@ -2,7 +2,7 @@ using API.Schema.MangaContext;
namespace API.Workers.MaintenanceWorkers;
public class CleanupMangaCoversWorker(IServiceScope scope, IEnumerable<BaseWorker>? dependsOn = null) : BaseWorkerWithContext<MangaContext>(scope, dependsOn), IPeriodic
public class CleanupMangaCoversWorker(IEnumerable<BaseWorker>? dependsOn = null) : BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
{
public DateTime LastExecution { get; set; } = DateTime.UtcNow;
public TimeSpan Interval { get; set; } = TimeSpan.FromMinutes(60);

View File

@ -1,10 +1,8 @@
using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
namespace API.Workers;
public class UpdateChaptersDownloadedWorker(Manga manga, IServiceScope scope, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(scope, dependsOn), IPeriodic
public class UpdateChaptersDownloadedWorker(Manga manga, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(dependsOn), IPeriodic
{
public DateTime LastExecution { get; set; } = DateTime.UtcNow;
public TimeSpan Interval { get; set; } = TimeSpan.FromMinutes(60);
@ -15,14 +13,7 @@ public class UpdateChaptersDownloadedWorker(Manga manga, IServiceScope scope, IE
mangaChapter.Downloaded = mangaChapter.CheckDownloaded();
}
try
{
DbContext.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
}
DbContext.Sync();
return [];
}
}

View File

@ -10,8 +10,8 @@ using static System.IO.UnixFileMode;
namespace API.Workers;
public class DownloadChapterFromMangaconnectorWorker(Chapter chapter, IServiceScope scope, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(scope, dependsOn)
public class DownloadChapterFromMangaconnectorWorker(Chapter chapter, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(dependsOn)
{
protected override BaseWorker[] DoWorkInternal()
{
@ -89,7 +89,7 @@ public class DownloadChapterFromMangaconnectorWorker(Chapter chapter, IServiceSc
Directory.Delete(tempFolder, true); //Cleanup
chapter.Downloaded = true;
DbContext.SaveChanges();
DbContext.Sync();
return [];
}

View File

@ -1,26 +1,20 @@
using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors;
using Microsoft.EntityFrameworkCore;
namespace API.Workers;
public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, IServiceScope scope, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(scope, dependsOn)
public class DownloadCoverFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(dependsOn)
{
public MangaConnectorId<Manga> MangaConnectorId { get; init; } = mcId;
protected override BaseWorker[] DoWorkInternal()
{
MangaConnector mangaConnector = MangaConnectorId.MangaConnector;
Manga manga = MangaConnectorId.Obj;
try
{
manga.CoverFileNameInCache = mangaConnector.SaveCoverImageToCache(MangaConnectorId);
DbContext.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
}
manga.CoverFileNameInCache = mangaConnector.SaveCoverImageToCache(MangaConnectorId);
DbContext.Sync();
return [];
}
}

View File

@ -1,11 +1,10 @@
using API.Schema.MangaContext;
using API.Schema.MangaContext.MangaConnectors;
using Microsoft.EntityFrameworkCore;
namespace API.Workers;
public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, string language, IServiceScope scope, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(scope, dependsOn)
public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId<Manga> mcId, string language, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(dependsOn)
{
public MangaConnectorId<Manga> MangaConnectorId { get; init; } = mcId;
protected override BaseWorker[] DoWorkInternal()
@ -19,20 +18,13 @@ public class RetrieveMangaChaptersFromMangaconnectorWorker(MangaConnectorId<Mang
manga.Chapters.Any(ch => chapter.Item1.Key == ch.Key && ch.Downloaded) == false).ToArray();
Log.Info($"{manga.Chapters.Count} existing + {newChapters.Length} new chapters.");
try
foreach ((Chapter chapter, MangaConnectorId<Chapter> mcId) newChapter in newChapters)
{
foreach ((Chapter chapter, MangaConnectorId<Chapter> mcId) newChapter in newChapters)
{
manga.Chapters.Add(newChapter.chapter);
DbContext.MangaConnectorToChapter.Add(newChapter.mcId);
}
manga.Chapters.Add(newChapter.chapter);
DbContext.MangaConnectorToChapter.Add(newChapter.mcId);
}
DbContext.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
}
DbContext.Sync();
return [];
}

View File

@ -1,24 +1,17 @@
using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
namespace API.Workers;
public class MoveMangaLibraryWorker(Manga manga, FileLibrary toLibrary, IServiceScope scope, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<MangaContext>(scope, dependsOn)
: BaseWorkerWithContext<MangaContext>(dependsOn)
{
protected override BaseWorker[] DoWorkInternal()
{
Dictionary<Chapter, string> oldPath = manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
manga.Library = toLibrary;
try
{
DbContext.SaveChanges();
}
catch (DbUpdateException e)
{
Log.Error(e);
if (DbContext.Sync().Result is { success: false })
return [];
}
return manga.Chapters.Select(c => new MoveFileOrFolderWorker(c.FullArchiveFilePath, oldPath[c])).ToArray<BaseWorker>();
}

View File

@ -2,8 +2,8 @@ using API.Schema.NotificationsContext;
namespace API.Workers;
public class SendNotificationsWorker(IServiceScope scope, IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<NotificationsContext>(scope, dependsOn), IPeriodic
public class SendNotificationsWorker(IEnumerable<BaseWorker>? dependsOn = null)
: BaseWorkerWithContext<NotificationsContext>(dependsOn), IPeriodic
{
public DateTime LastExecution { get; set; } = DateTime.UtcNow;
public TimeSpan Interval { get; set; } = TimeSpan.FromMinutes(1);