diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/JsonNetAuditSerializer.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/JsonNetAuditSerializer.cs index 8cc44e0888..3862dd01da 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/JsonNetAuditSerializer.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/JsonNetAuditSerializer.cs @@ -4,6 +4,7 @@ using Volo.Abp.DependencyInjection; namespace Volo.Abp.Auditing { + //TODO: Rename to JsonAuditSerializer public class JsonNetAuditSerializer : IAuditSerializer, ITransientDependency { protected AbpAuditingOptions Options; diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo.Abp.BackgroundJobs.csproj b/framework/src/Volo.Abp.BackgroundJobs/Volo.Abp.BackgroundJobs.csproj index cc4ee2e787..90fb7bb890 100644 --- a/framework/src/Volo.Abp.BackgroundJobs/Volo.Abp.BackgroundJobs.csproj +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo.Abp.BackgroundJobs.csproj @@ -14,7 +14,11 @@ + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobsModule.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobsModule.cs index a7bcf94526..6590b4788a 100644 --- a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobsModule.cs +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobsModule.cs @@ -1,8 +1,18 @@ using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.BackgroundWorkers; +using Volo.Abp.Guids; +using Volo.Abp.Json; using Volo.Abp.Modularity; +using Volo.Abp.Timing; namespace Volo.Abp.BackgroundJobs { + [DependsOn( + typeof(AbpBackgroundWorkersModule), + typeof(AbpTimingModule), + typeof(AbpJsonModule), + typeof(AbpGuidsModule) + )] public class AbpBackgroundJobsModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) @@ -10,4 +20,4 @@ namespace Volo.Abp.BackgroundJobs context.Services.AddAssemblyOf(); } } -} +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJob.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJob.cs new file mode 100644 index 0000000000..9ed0633806 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJob.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Volo.Abp.BackgroundJobs +{ + public abstract class BackgroundJob : IBackgroundJob + { + //TODO: Add UOW, Localization and other useful properties..? + + public ILogger> Logger { get; set; } + + protected BackgroundJob() + { + Logger = NullLogger>.Instance; + } + + public abstract void Execute(TArgs args); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobException.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobException.cs new file mode 100644 index 0000000000..8e16d983bd --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobException.cs @@ -0,0 +1,44 @@ +using System; +using System.Runtime.Serialization; +using JetBrains.Annotations; + +namespace Volo.Abp.BackgroundJobs +{ + [Serializable] + public class BackgroundJobException : AbpException + { + [CanBeNull] + public BackgroundJobInfo BackgroundJob { get; set; } + + [CanBeNull] + public object JobObject { get; set; } + + /// + /// Creates a new object. + /// + public BackgroundJobException() + { + + } + + /// + /// Creates a new object. + /// + public BackgroundJobException(SerializationInfo serializationInfo, StreamingContext context) + : base(serializationInfo, context) + { + + } + + /// + /// Creates a new object. + /// + /// Exception message + /// Inner exception + public BackgroundJobException(string message, Exception innerException) + : base(message, innerException) + { + + } + } +} diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobInfo.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobInfo.cs new file mode 100644 index 0000000000..de9e4d83a6 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobInfo.cs @@ -0,0 +1,126 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Timing; + +namespace Volo.Abp.BackgroundJobs +{ + /// + /// Represents a background job info that is used to persist jobs. + /// + public class BackgroundJobInfo + { + /// + /// Maximum length of . + /// Value: 512. + /// + public const int MaxJobTypeLength = 512; + + /// + /// Maximum length of . + /// Value: 1 MB (1,048,576 bytes). + /// + public const int MaxJobArgsLength = 1024 * 1024; + + /// + /// Default duration (as seconds) for the first wait on a failure. + /// Default value: 60 (1 minutes). + /// + public static int DefaultFirstWaitDuration { get; set; } //TODO: Move to configuration + + /// + /// Default timeout value (as seconds) for a job before it's abandoned (). + /// Default value: 172,800 (2 days). + /// + public static int DefaultTimeout { get; set; } //TODO: Move to configuration + + /// + /// Default wait factor for execution failures. + /// This amount is multiplated by last wait time to calculate next wait time. + /// Default value: 2.0. + /// + public static double DefaultWaitFactor { get; set; } //TODO: Move to configuration + + public Guid Id { get; set; } + + /// + /// Type of the job. + /// It's AssemblyQualifiedName of job type. + /// + [Required] + [StringLength(MaxJobTypeLength)] + public virtual string JobType { get; set; } + + /// + /// Job arguments as JSON string. + /// + [Required] + [MaxLength(MaxJobArgsLength)] + public virtual string JobArgs { get; set; } //TODO: Consider to conver to byte[] + + /// + /// Try count of this job. + /// A job is re-tried if it fails. + /// + public virtual short TryCount { get; set; } + + /// + /// Creation time of this job. + /// + public virtual DateTime CreationTime { get; set; } + + /// + /// Next try time of this job. + /// + public virtual DateTime NextTryTime { get; set; } + + /// + /// Last try time of this job. + /// + public virtual DateTime? LastTryTime { get; set; } + + /// + /// This is true if this job is continously failed and will not be executed again. + /// + public virtual bool IsAbandoned { get; set; } + + /// + /// Priority of this job. + /// + public virtual BackgroundJobPriority Priority { get; set; } + + static BackgroundJobInfo() + { + DefaultFirstWaitDuration = 60; + DefaultTimeout = 172800; + DefaultWaitFactor = 2.0; + } + + /// + /// Initializes a new instance of the class. + /// + public BackgroundJobInfo() + { + Priority = BackgroundJobPriority.Normal; + } + + /// + /// Calculates next try time if a job fails. + /// Returns null if it will not wait anymore and job should be abandoned. + /// + /// + public virtual DateTime? CalculateNextTryTime(IClock clock) //TODO: Move to another place to override easier + { + var nextWaitDuration = DefaultFirstWaitDuration * (Math.Pow(DefaultWaitFactor, TryCount - 1)); + var nextTryDate = LastTryTime.HasValue + ? LastTryTime.Value.AddSeconds(nextWaitDuration) + : clock.Now.AddSeconds(nextWaitDuration); + + if (nextTryDate.Subtract(CreationTime).TotalSeconds > DefaultTimeout) + { + return null; + } + + return nextTryDate; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobManager.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobManager.cs new file mode 100644 index 0000000000..bd9f729a5d --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobManager.cs @@ -0,0 +1,182 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.BackgroundWorkers; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Guids; +using Volo.Abp.Threading; +using Volo.Abp.Timing; + +namespace Volo.Abp.BackgroundJobs +{ + /// + /// Default implementation of . + /// + public class BackgroundJobManager : PeriodicBackgroundWorkerBase, IBackgroundJobManager, ISingletonDependency + { + /// + /// Interval between polling jobs from . + /// Default value: 5000 (5 seconds). + /// + public static int JobPollPeriod { get; set; } //TODO: Move to options + + protected IServiceProvider ServiceProvider { get; } + protected IClock Clock { get; } + protected IBackgroundJobSerializer Serializer { get; } + protected IGuidGenerator GuidGenerator { get; } + protected IBackgroundJobStore Store { get; } + + static BackgroundJobManager() + { + JobPollPeriod = 5000; + } + + /// + /// Initializes a new instance of the class. + /// + public BackgroundJobManager( + IServiceProvider serviceProvider, + IClock clock, + IBackgroundJobSerializer serializer, + IBackgroundJobStore store, + IGuidGenerator guidGenerator, + AbpTimer timer) + : base(timer) + { + ServiceProvider = serviceProvider; + Clock = clock; + Serializer = serializer; + GuidGenerator = guidGenerator; + Store = store; + + Timer.Period = JobPollPeriod; + } + + public async Task EnqueueAsync(TArgs args, BackgroundJobPriority priority = BackgroundJobPriority.Normal, TimeSpan? delay = null) + where TJob : IBackgroundJob + { + var jobInfo = new BackgroundJobInfo + { + Id = GuidGenerator.Create(), + JobType = typeof(TJob).AssemblyQualifiedName, + JobArgs = Serializer.Serialize(args), + Priority = priority, + CreationTime = Clock.Now, + NextTryTime = Clock.Now + }; + + if (delay.HasValue) + { + jobInfo.NextTryTime = Clock.Now.Add(delay.Value); + } + + await Store.InsertAsync(jobInfo); + + return jobInfo.Id; + } + + public async Task DeleteAsync(Guid jobId) + { + var jobInfo = await Store.GetAsync(jobId); + if (jobInfo == null) + { + return false; + } + + await Store.DeleteAsync(jobInfo); + return true; + } + + protected override void DoWork() + { + var waitingJobs = AsyncHelper.RunSync(() => Store.GetWaitingJobsAsync(1000)); + + foreach (var job in waitingJobs) + { + TryProcessJob(job); + } + } + + private void TryProcessJob(BackgroundJobInfo jobInfo) + { + try + { + jobInfo.TryCount++; + jobInfo.LastTryTime = Clock.Now; + + var jobType = Type.GetType(jobInfo.JobType); + using (var scope = ServiceProvider.CreateScope()) + { + var job = scope.ServiceProvider.GetService(jobType); + if (job == null) + { + throw new AbpException("JobType is not registered: " + jobType); + } + + //TODO: Type check for the job object + + var jobExecuteMethod = job.GetType().GetMethod("Execute"); + Debug.Assert(jobExecuteMethod != null, nameof(jobExecuteMethod) + " != null"); + var argsType = jobExecuteMethod.GetParameters()[0].ParameterType; + var argsObj = Serializer.Deserialize(jobInfo.JobArgs, argsType); + + try + { + jobExecuteMethod.Invoke(job, new[] { argsObj }); + AsyncHelper.RunSync(() => Store.DeleteAsync(jobInfo)); + } + catch (Exception ex) + { + Logger.LogException(ex); + + var nextTryTime = jobInfo.CalculateNextTryTime(Clock); + if (nextTryTime.HasValue) + { + jobInfo.NextTryTime = nextTryTime.Value; + } + else + { + jobInfo.IsAbandoned = true; + } + + TryUpdate(jobInfo); + + var backgroundJobException = new BackgroundJobException( + "A background job execution is failed. See inner exception for details. See BackgroundJob property to get information on the background job.", + ex + ) + { + BackgroundJob = jobInfo, + JobObject = job + }; + + //TODO: Somehow trigger an event for the exception (may create an Volo.Abp.ExceptionHandling package)! + } + } + } + catch (Exception ex) + { + Logger.LogException(ex); + + jobInfo.IsAbandoned = true; + + TryUpdate(jobInfo); + } + } + + private void TryUpdate(BackgroundJobInfo jobInfo) + { + try + { + Store.UpdateAsync(jobInfo); + } + catch (Exception updateEx) + { + Logger.LogException(updateEx); + } + } + } +} diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobManagerExtensions.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobManagerExtensions.cs new file mode 100644 index 0000000000..b6cbca221b --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobManagerExtensions.cs @@ -0,0 +1,26 @@ +using System; +using Volo.Abp.Threading; + +namespace Volo.Abp.BackgroundJobs +{ + /// + /// Some extension methods for . + /// + public static class BackgroundJobManagerExtensions + { + /// + /// Enqueues a job to be executed. + /// + /// Type of the job. + /// Type of the arguments of job. + /// Background job manager reference + /// Job arguments. + /// Job priority. + /// Job delay (wait duration before first try). + public static void Enqueue(this IBackgroundJobManager backgroundJobManager, TArgs args, BackgroundJobPriority priority = BackgroundJobPriority.Normal, TimeSpan? delay = null) + where TJob : IBackgroundJob + { + AsyncHelper.RunSync(() => backgroundJobManager.EnqueueAsync(args, priority, delay)); + } + } +} diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobOptions.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobOptions.cs new file mode 100644 index 0000000000..abd89e6e40 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobOptions.cs @@ -0,0 +1,12 @@ +namespace Volo.Abp.BackgroundJobs +{ + public class BackgroundJobOptions + { + public bool IsJobExecutionEnabled { get; set; } + + public BackgroundJobOptions() + { + IsJobExecutionEnabled = true; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobPriority.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobPriority.cs new file mode 100644 index 0000000000..7c5f28dbeb --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobPriority.cs @@ -0,0 +1,33 @@ +namespace Volo.Abp.BackgroundJobs +{ + /// + /// Priority of a background job. + /// + public enum BackgroundJobPriority : byte + { + /// + /// Low. + /// + Low = 5, + + /// + /// Below normal. + /// + BelowNormal = 10, + + /// + /// Normal (default). + /// + Normal = 15, + + /// + /// Above normal. + /// + AboveNormal = 20, + + /// + /// High. + /// + High = 25 + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJob.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJob.cs new file mode 100644 index 0000000000..e7c942ec4c --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJob.cs @@ -0,0 +1,14 @@ +namespace Volo.Abp.BackgroundJobs +{ + /// + /// Defines interface of a background job. + /// + public interface IBackgroundJob + { + /// + /// Executes the job with the . + /// + /// Job arguments. + void Execute(TArgs args); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobManager.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobManager.cs new file mode 100644 index 0000000000..526e6c84d6 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobManager.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.BackgroundWorkers; + +namespace Volo.Abp.BackgroundJobs +{ + //TODO: Create a non-generic EnqueueAsync extension method to IBackgroundJobManager which takes types as input parameters rather than generic parameters. + /// + /// Defines interface of a job manager. + /// + public interface IBackgroundJobManager : IBackgroundWorker + { + /// + /// Enqueues a job to be executed. + /// + /// Type of the job. + /// Type of the arguments of job. + /// Job arguments. + /// Job priority. + /// Job delay (wait duration before first try). + /// Unique identifier of a background job. + Task EnqueueAsync(TArgs args, BackgroundJobPriority priority = BackgroundJobPriority.Normal, TimeSpan? delay = null) + where TJob : IBackgroundJob; + + /// + /// Deletes a job with the specified jobId. + /// + /// The Job Unique Identifier. + /// True on a successfull state transition, false otherwise. + Task DeleteAsync(Guid jobId); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobSerializer.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobSerializer.cs new file mode 100644 index 0000000000..b0e80001ad --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobSerializer.cs @@ -0,0 +1,11 @@ +using System; + +namespace Volo.Abp.BackgroundJobs +{ + public interface IBackgroundJobSerializer + { + string Serialize(object obj); + + object Deserialize(string value, Type type); + } +} diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobStore.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobStore.cs new file mode 100644 index 0000000000..1c18d2a273 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobStore.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Volo.Abp.BackgroundJobs +{ + /// + /// Defines interface to store/get background jobs. + /// + public interface IBackgroundJobStore + { + /// + /// Gets a BackgroundJobInfo based on the given jobId. + /// + /// The Job Unique Identifier. + /// The BackgroundJobInfo object. + Task GetAsync(Guid jobId); + + /// + /// Inserts a background job. + /// + /// Job information. + Task InsertAsync(BackgroundJobInfo jobInfo); + + /// + /// Gets waiting jobs. It should get jobs based on these: + /// Conditions: !IsAbandoned And NextTryTime <= Clock.Now. + /// Order by: Priority DESC, TryCount ASC, NextTryTime ASC. + /// Maximum result: . + /// + /// Maximum result count. + Task> GetWaitingJobsAsync(int maxResultCount); + + /// + /// Deletes a job. + /// + /// Job information. + Task DeleteAsync(BackgroundJobInfo jobInfo); + + /// + /// Updates a job. + /// + /// Job information. + Task UpdateAsync(BackgroundJobInfo jobInfo); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/JsonBackgroundJobSerializer.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/JsonBackgroundJobSerializer.cs new file mode 100644 index 0000000000..361058e5c8 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/JsonBackgroundJobSerializer.cs @@ -0,0 +1,30 @@ +using System; +using Volo.Abp.Json; + +namespace Volo.Abp.BackgroundJobs +{ + public class JsonBackgroundJobSerializer : IBackgroundJobSerializer + { + private readonly IJsonSerializer _jsonSerializer; + + public JsonBackgroundJobSerializer(IJsonSerializer jsonSerializer) + { + _jsonSerializer = jsonSerializer; + } + + public string Serialize(object obj) + { + return _jsonSerializer.Serialize(obj); + } + + public object Deserialize(string value, Type type) + { + return _jsonSerializer.Deserialize(type, value); + } + + public T Deserialize(string value) + { + return _jsonSerializer.Deserialize(value); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/NullBackgroundJobStore.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/NullBackgroundJobStore.cs new file mode 100644 index 0000000000..42a6656567 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/NullBackgroundJobStore.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Volo.Abp.BackgroundJobs +{ + /// + /// Null pattern implementation of . + /// It's used if is not implemented by actual persistent store + /// and job execution is not enabled () for the application. + /// + public class NullBackgroundJobStore : IBackgroundJobStore + { + public Task GetAsync(Guid jobId) + { + return Task.FromResult((BackgroundJobInfo)null); + } + + public Task InsertAsync(BackgroundJobInfo jobInfo) + { + return Task.CompletedTask; + } + + public Task> GetWaitingJobsAsync(int maxResultCount) + { + return Task.FromResult(new List()); + } + + public Task DeleteAsync(BackgroundJobInfo jobInfo) + { + return Task.CompletedTask; + } + + public Task UpdateAsync(BackgroundJobInfo jobInfo) + { + return Task.CompletedTask; + } + } +} \ No newline at end of file