refactor: update core project structure and dependencies

Updates project to target .NET 6-9, adds ErrorOr package, standardizes naming
and copyright info, simplifies command executor interfaces, removes legacy
ExecutorResult in favor of ApiResult pattern
develop
Shaoming,Wang 10 months ago
parent 1e7dd0f239
commit 517af98597
  1. 6
      .editorconfig
  2. 15
      DotXxlJob.sln
  3. 2
      LICENSE
  4. 72
      README.md
  5. 6512
      doc/class-diagram-for-v2.excalidraw
  6. 123
      doc/class-diagram-for-v2.puml
  7. 1
      doc/class-diagram-for-v2.svg
  8. 127
      doc/class-diagram-for-v3.puml
  9. 1
      doc/class-diagram-for-v3.svg
  10. 7
      src/DotXxlJob.Core/CommandExecutors/BeatCommandExecutor.cs
  11. 28
      src/DotXxlJob.Core/CommandExecutors/CommandExecutorFactory.cs
  12. 7
      src/DotXxlJob.Core/CommandExecutors/IdleBeatCommandExecutor.cs
  13. 7
      src/DotXxlJob.Core/CommandExecutors/KillCommandExecutor.cs
  14. 9
      src/DotXxlJob.Core/CommandExecutors/TriggerCommandExecutor.cs
  15. 2
      src/DotXxlJob.Core/DependencyExtensions.cs
  16. 5
      src/DotXxlJob.Core/DotXxlJob.Core.csproj
  17. 5
      src/DotXxlJob.Core/ICommandExecutor.cs
  18. 4
      src/DotXxlJob.Core/ICommandExecutorFactory.cs
  19. 19
      src/DotXxlJob.Core/IJobDispatcher.cs
  20. 16
      src/DotXxlJob.Core/IJobExecutor.cs
  21. 11
      src/DotXxlJob.Core/IJobExecutorFactory.cs
  22. 12
      src/DotXxlJob.Core/IJobLogger.cs
  23. 2
      src/DotXxlJob.Core/ISerializer.cs
  24. 16
      src/DotXxlJob.Core/Internal/CallbackWorker.cs
  25. 14
      src/DotXxlJob.Core/Internal/JobCompletedArgs.cs
  26. 58
      src/DotXxlJob.Core/Internal/JobDispatcher.cs
  27. 26
      src/DotXxlJob.Core/Internal/JobExecutorFactory.cs
  28. 134
      src/DotXxlJob.Core/Internal/JobQueue.cs
  29. 97
      src/DotXxlJob.Core/Internal/JobWorker.cs
  30. 2
      src/DotXxlJob.Core/Internal/TextJsonSerializer.cs
  31. 15
      src/DotXxlJob.Core/JobExecuteContext.cs
  32. 42
      src/DotXxlJob.Core/Models/ApiResult.cs
  33. 12
      src/DotXxlJob.Core/Models/ExecutorBlockStrategy.cs
  34. 40
      src/DotXxlJob.Core/Models/ExecutorResult.cs
  35. 2
      src/DotXxlJob.Core/Models/IdleBeatCommand.cs
  36. 22
      src/DotXxlJob.Core/Models/Job.cs
  37. 2
      src/DotXxlJob.Core/Models/KillCommand.cs
  38. 41
      src/DotXxlJob.Core/Models/TaskResult.cs
  39. 2
      src/DotXxlJob.Core/Models/TriggerCommand.cs
  40. 12
      src/DotXxlJob.Core/Properties/launchSettings.json
  41. 67
      src/DotXxlJob.Core/XxlJobExecutorOptions.cs
  42. 111
      src/DotXxlJob.Core/XxlJobHttpHandler.cs
  43. 44
      tests/DotXxlJob.Core.UnitTests/CommandExecutorFactoryTests.cs
  44. 28
      tests/DotXxlJob.Core.UnitTests/DotXxlJob.Core.UnitTests.csproj
  45. 84
      tests/DotXxlJob.Core.UnitTests/XxlJobHttpHandlerTests.cs

@ -4,7 +4,7 @@
# You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference
[*.cs]
file_header_template = Copyright (c) Xuanye Wong. All rights reserved.\nLicensed under MIT license
file_header_template = Copyright (c) Xuanye Wang. All rights reserved.\nLicensed under MIT license
dotnet_diagnostic.IDE0073.severity = warning
#Core editorconfig formatting - indentation
@ -20,8 +20,8 @@ csharp_new_line_before_open_brace = all
#Formatting - organize using options
#do not place System.* using directives before other using directives
dotnet_sort_system_directives_first = false
#do place System.* using directives before other using directives
dotnet_sort_system_directives_first = true
#Formatting - spacing options

@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCoreExecutor", "sampl
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotXxlJob.Core", "src\DotXxlJob.Core\DotXxlJob.Core.csproj", "{4584B4D5-0DA9-425F-A4C7-7A19A75D3E73}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotXxlJob.Core.UnitTests", "tests\DotXxlJob.Core.UnitTests\DotXxlJob.Core.UnitTests.csproj", "{7D0FB732-636F-4B72-B1E9-262077F1DB82}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -47,6 +49,18 @@ Global
{4584B4D5-0DA9-425F-A4C7-7A19A75D3E73}.Release|x64.Build.0 = Release|Any CPU
{4584B4D5-0DA9-425F-A4C7-7A19A75D3E73}.Release|x86.ActiveCfg = Release|Any CPU
{4584B4D5-0DA9-425F-A4C7-7A19A75D3E73}.Release|x86.Build.0 = Release|Any CPU
{7D0FB732-636F-4B72-B1E9-262077F1DB82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D0FB732-636F-4B72-B1E9-262077F1DB82}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D0FB732-636F-4B72-B1E9-262077F1DB82}.Debug|x64.ActiveCfg = Debug|Any CPU
{7D0FB732-636F-4B72-B1E9-262077F1DB82}.Debug|x64.Build.0 = Debug|Any CPU
{7D0FB732-636F-4B72-B1E9-262077F1DB82}.Debug|x86.ActiveCfg = Debug|Any CPU
{7D0FB732-636F-4B72-B1E9-262077F1DB82}.Debug|x86.Build.0 = Debug|Any CPU
{7D0FB732-636F-4B72-B1E9-262077F1DB82}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D0FB732-636F-4B72-B1E9-262077F1DB82}.Release|Any CPU.Build.0 = Release|Any CPU
{7D0FB732-636F-4B72-B1E9-262077F1DB82}.Release|x64.ActiveCfg = Release|Any CPU
{7D0FB732-636F-4B72-B1E9-262077F1DB82}.Release|x64.Build.0 = Release|Any CPU
{7D0FB732-636F-4B72-B1E9-262077F1DB82}.Release|x86.ActiveCfg = Release|Any CPU
{7D0FB732-636F-4B72-B1E9-262077F1DB82}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -54,6 +68,7 @@ Global
GlobalSection(NestedProjects) = preSolution
{942A8837-BBAB-4DC6-8ABB-4E3B7AD3EB4E} = {E959F9B5-F3EB-48B1-B842-2CDDFDB01900}
{4584B4D5-0DA9-425F-A4C7-7A19A75D3E73} = {97756BA5-1E7C-4536-A49E-AE2190C0E6A5}
{7D0FB732-636F-4B72-B1E9-262077F1DB82} = {352EC932-F112-4A2F-9DC3-F0761C85E068}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F4A8B63E-6284-4D00-9719-BAB1D955DACF}

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 xuanye wong
Copyright (c) 2019 Xuanye Wang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

@ -156,78 +156,6 @@ public class DemoJobHandler:AbstractJobHandler
}
```
```
classDiagram
class IJobHandler {
<<interface>>
+Execute(JobExecuteContext) Task<ReturnT>
+Init()
+Destroy()
}
class AbstractJobHandler {
<<abstract>>
+Execute(JobExecuteContext)* Task<ReturnT>
+Init()
+Destroy()
}
class JobHandlerAttribute {
+Name: string
}
class XxlJobExecutorOptions {
+AdminAddresses: string
+AppName: string
+SpecialBindAddress: string
+Port: int
+AutoRegistry: bool
+AccessToken: string
+LogPath: string
+LogRetentionDays: int
}
class JobExecuteContext {
+JobId: int
+JobParameter: string
+ShardIndex: int
+ShardTotal: int
+JobLogger: IJobLogger
}
class ReturnT {
+Code: int
+Msg: string
+Content: object
+SUCCESS: ReturnT
+FAIL: ReturnT
}
class XxlJobExecutorMiddleware {
-IJobHandlerFactory _jobHandlerFactory
+InvokeAsync(HttpContext)
}
class IJobHandlerFactory {
<<interface>>
+GetJobHandler(string) IJobHandler
}
class DefaultJobHandlerFactory {
-IServiceProvider _serviceProvider
+GetJobHandler(string) IJobHandler
}
IJobHandler <|.. AbstractJobHandler
AbstractJobHandler <|-- DemoJobHandler
IJobHandler -- JobHandlerAttribute
IJobHandlerFactory <|.. DefaultJobHandlerFactory
XxlJobExecutorMiddleware --> IJobHandlerFactory
IJobHandler --> JobExecuteContext
IJobHandler --> ReturnT
```
## 其他说明
注意XXL-JOB 2.0.1版本请使用 1.0.8的执行器实现

File diff suppressed because it is too large Load Diff

@ -0,0 +1,123 @@
@startuml XxlJob.Core Class Diagram
title XxlJob.Core Class Diagram
class XxlRestfulServiceHandler {
- JobDispatcher _jobDispatcher
- IJobLogger _jobLogger
+ HandleAync(HttpContext context)
}
interface IJobLogger {
+ SetLogFile(long logTime, long logId)
+ Log(string pattern, params object[] format)
+ LogError(Exception ex)
+ ReadLog(long logTime, long logId, int fromLine)
+ LogSpecialFile()
}
class JobDispatcher {
- TaskExecutorFactory _executorFactory
- CallbackQueue _callbackTaskQueue
- ConcurrentDictionary<int, JobTaskQueue> RUNNING_QUEUE
+ TryRemoveJobTask(int jobId)
+ Execute(TriggerParam triggerParam)
+ IdleBeat(int jobId)
}
class TaskExecutorFactory {
- Dictionary<string, ITaskExecutor> _cache
+ GetTaskExecutor(string glueType)
}
class CallbackQueue {
- AdminClient _adminClient
- IJobLogger _jobLogger
- RetryCallbackTaskQueue _retryQueue
- ConcurrentQueue<HandleCallbackParam> taskQueue
+ Push(HandleCallbackParam callbackParam)
+ Dispose()
}
class RetryCallbackTaskQueue {}
class AdminClient {
}
class JobTaskQueue {
- ConcurrentQueue<TriggerParam> TASK_QUEUE
- ConcurrentDictionary<long, byte> ID_IN_QUEUE
+ EventHandler<HandleCallbackParam> CallBack
+ IsRunning()
+ Replace(TriggerParam triggerParam)
+ Push(TriggerParam triggerParam)
+ Stop()
+ Dispose()
}
interface ITaskExecutor {
+ Execute(TriggerParam triggerParam)
}
class BeanTaskExecutor {
- IJobHandlerFactory _handlerFactory
- IJobLogger _jobLogger
}
class TriggerParam {}
class HandleCallbackParam {}
interface IJobHandlerFactory {}
class JobHandlerFactory {
+ GetJobHandler(string handlerName)
}
interface IJobHandler {
+ Execute(JobExecuteContext context)
}
abstract class AbstractJobHandler {}
class JobExecuteContext {
+ string JobParameter
+ IJobLogger JobLogger
+ CancellationToken cancellationToken
}
class JobHandlerAttribute {}
note "用于标记JobHandler的名字" as N1
XxlRestfulServiceHandler -right-> JobDispatcher
XxlRestfulServiceHandler --> IJobLogger
JobDispatcher --> TaskExecutorFactory
JobDispatcher --> CallbackQueue
JobDispatcher --> JobTaskQueue
TaskExecutorFactory *-- ITaskExecutor
BeanTaskExecutor -up-|> ITaskExecutor
CallbackQueue --> IJobLogger
CallbackQueue --> RetryCallbackTaskQueue
CallbackQueue --> AdminClient
CallbackQueue --> HandleCallbackParam
BeanTaskExecutor --> IJobHandlerFactory
JobHandlerFactory -right-|> IJobHandlerFactory
JobHandlerFactory *-left- IJobHandler
AbstractJobHandler -right-|> IJobHandler
N1 .. JobHandlerAttribute
@enduml

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 41 KiB

@ -0,0 +1,127 @@
@startuml XxlJob.Core - Class Diagram
title XxlJob.Core - Class Diagram
class XxlJobHttpHandler{
+ HandleAsync(HttpContext context)
}
interface ICommandExecutorFactory{
+ GetCommandExecutor(string commandName)
}
class CommandExecutorFactory{}
interface ICommandExecutor{
+ string CommandName
+ ExecuteAsync(byte[] payload)
}
class BeatCommandExecutor{}
class IdleBeatCommandExecutor{}
class TiggerCommandExecutor{}
class AbortCommandExecutor{}
interface IJobDispatcher{
+ StartAsync()
+ StopAsync()
+ DisposeAsync()
+ TiggerJobAsync(Job job)
+ ReplaceJobAsync(Job job)
+ AbortJobAsync(int jobId)
}
class JobDispatcher{
- JobQueue _jobQueue
- CallQueue _callbackQueue
}
interface IJobExecutorFactory{
+ GetTaskExecutor(string glueType)
}
class JobExecutorFactory{
}
interface IJobExecutor{
+ string GlueType
+ ExecuteAsync(Job job)
}
class BeanJobExecutor{
}
interface IJobHandlerFactory{
+ GetJobHandler(string handlerName)
}
class JobHandlerFactory{
}
interface IJobHandler{
+ string Name
+ HandleAsync(JobContext context)
}
class JobWorker{
- Queue _jobQueue
+ DisposeAsync()
+ Event OnJobCompleted
+ EnqueueTaskAsync(JobTask task)
+ ReplaceJobAsync(Job job)
+ AbortJobAsync(JobId jobId)
}
class JobExecuteResult{}
class CallBackWorker{
- Queue _callbackQueue
+ DisposeAsync()
+ EnqueueAsync(JobExecuteResult result)
}
interface IAdminClient{}
XxlJobHttpHandler ..> ICommandExecutorFactory
CommandExecutorFactory -up-|> ICommandExecutorFactory
BeatCommandExecutor -up-|> ICommandExecutor
IdleBeatCommandExecutor -up-|> ICommandExecutor
TiggerCommandExecutor -up-|> ICommandExecutor
AbortCommandExecutor -up-|> ICommandExecutor
ICommandExecutor .up.* CommandExecutorFactory
BeanJobExecutor -right-|> IJobExecutor
IJobExecutor .right.* JobExecutorFactory
JobDispatcher -up-|> IJobDispatcher
JobDispatcher ..> JobWorker
JobDispatcher ..> CallBackWorker
JobWorker ..> IJobExecutorFactory
CallBackWorker ..> IAdminClient
JobExecutorFactory -up-|> IJobExecutorFactory
BeatCommandExecutor ..> IJobDispatcher
IdleBeatCommandExecutor ..> IJobDispatcher
TiggerCommandExecutor ..> IJobDispatcher
AbortCommandExecutor ..> IJobDispatcher
BeanJobExecutor ..> IJobHandlerFactory
JobHandlerFactory -up-|> IJobHandlerFactory
IJobHandler ..* JobHandlerFactory
@enduml

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 45 KiB

@ -1,8 +1,7 @@
// Copyright (c) Xuanye Wong. All rights reserved.
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using DotXxlJob.Core.Models;
using System.Threading;
using System.Threading.Tasks;
namespace DotXxlJob.Core.CommandExecutors
@ -11,9 +10,9 @@ namespace DotXxlJob.Core.CommandExecutors
{
public string CommandName => "Beat";
public Task<ExecutorResult> ExecuteAsync(byte[] payload,CancellationToken cancellationToken = default)
public Task<ApiResult> ExecuteAsync(byte[] payload)
{
return Task.FromResult(ExecutorResult.Success());
return Task.FromResult(ApiResult.Success());
}
}
}

@ -0,0 +1,28 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using System.Collections.Generic;
using System.Linq;
namespace DotXxlJob.Core.CommandExecutors
{
public class CommandExecutorFactory : ICommandExecutorFactory
{
private readonly Dictionary<string, ICommandExecutor> _commandExecutors;
public CommandExecutorFactory(IEnumerable<ICommandExecutor> commandExecutors)
{
_commandExecutors = commandExecutors.ToDictionary(x => x.CommandName, y => y);
}
public ICommandExecutor? GetCommandExecutor(string commandName)
{
if (_commandExecutors.TryGetValue(commandName, out var commandExecutor))
{
return commandExecutor;
}
return null;
}
}
}

@ -1,8 +1,7 @@
// Copyright (c) Xuanye Wong. All rights reserved.
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using DotXxlJob.Core.Models;
using System.Threading;
using System.Threading.Tasks;
namespace DotXxlJob.Core.CommandExecutors
@ -17,12 +16,12 @@ namespace DotXxlJob.Core.CommandExecutors
}
public string CommandName => "IdleBeat";
public Task<ExecutorResult> ExecuteAsync(byte[] payload,CancellationToken cancellationToken = default)
public Task<ApiResult> ExecuteAsync(byte[] payload)
{
var idleBeat = _serializer.Deserialize<IdleBeatCommand>(payload);
if (idleBeat == null)
{
return Task.FromResult(ExecutorResult.Failure("Command[IdleBrat],parameter is empty"));
return Task.FromResult(ApiResult.Failure("Command[IdleBrat],parameter is empty"));
}
throw new System.NotImplementedException();
}

@ -1,8 +1,7 @@
// Copyright (c) Xuanye Wong. All rights reserved.
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using DotXxlJob.Core.Models;
using System.Threading;
using System.Threading.Tasks;
namespace DotXxlJob.Core.CommandExecutors
@ -16,12 +15,12 @@ namespace DotXxlJob.Core.CommandExecutors
_serializer = serializer;
}
public string CommandName => "kill";
public Task<ExecutorResult> ExecuteAsync(byte[] payload, CancellationToken cancellationToken)
public Task<ApiResult> ExecuteAsync(byte[] payload)
{
var command = _serializer.Deserialize<IdleBeatCommand>(payload);
if (command == null)
{
return Task.FromResult(ExecutorResult.Failure("Command[Kill],parameter is empty"));
return Task.FromResult(ApiResult.Failure("Command[Kill],parameter is empty"));
}
throw new System.NotImplementedException();
}

@ -1,9 +1,8 @@
// Copyright (c) Xuanye Wong. All rights reserved.
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using DotXxlJob.Core.Models;
using System.Threading;
using System.Threading.Tasks;
using DotXxlJob.Core.Models;
namespace DotXxlJob.Core.CommandExecutors
{
@ -16,12 +15,12 @@ namespace DotXxlJob.Core.CommandExecutors
_serializer = serializer;
}
public string CommandName => "Run";
public Task<ExecutorResult> ExecuteAsync(byte[] payload, CancellationToken cancellationToken)
public Task<ApiResult> ExecuteAsync(byte[] payload)
{
var command = _serializer.Deserialize<TriggerCommand>(payload);
if (command == null)
{
return Task.FromResult(ExecutorResult.Failure("command[run],parameter is empty"));
return Task.FromResult(ApiResult.Failure("command[run],parameter is empty"));
}
throw new System.NotImplementedException();
}

@ -1,4 +1,4 @@
// Copyright (c) Xuanye Wong. All rights reserved.
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
namespace DotXxlJob.Core

@ -1,11 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="ErrorOr" Version="2.0.1" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>

@ -1,8 +1,7 @@
// Copyright (c) Xuanye Wong. All rights reserved.
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using DotXxlJob.Core.Models;
using System.Threading;
using System.Threading.Tasks;
namespace DotXxlJob.Core
@ -10,7 +9,7 @@ namespace DotXxlJob.Core
public interface ICommandExecutor
{
string CommandName { get; }
Task<ExecutorResult> ExecuteAsync(byte[] payload,CancellationToken cancellationToken);
Task<ApiResult> ExecuteAsync(byte[] payload);
}

@ -1,10 +1,10 @@
// Copyright (c) Xuanye Wong. All rights reserved.
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
namespace DotXxlJob.Core
{
public interface ICommandExecutorFactory
{
ICommandExecutor GetCommandExecutor(string commandName);
ICommandExecutor? GetCommandExecutor(string commandName);
}
}

@ -0,0 +1,19 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using System.Threading;
using System.Threading.Tasks;
using DotXxlJob.Core.Models;
namespace DotXxlJob.Core
{
public interface IJobDispatcher
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
Task AddJobAsync(Job job);
Task ReplaceJobAsync(Job job);
Task AbortJobAsync(JobId jobId);
}
}

@ -0,0 +1,16 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using System.Threading;
using System.Threading.Tasks;
using DotXxlJob.Core.Models;
namespace DotXxlJob.Core
{
public interface IJobExecutor
{
string GlueType { get; }
Task<TaskResult> ExecuteAsync(JobExecuteContext context, Job job, CancellationToken cancellationToken);
}
}

@ -0,0 +1,11 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
namespace DotXxlJob.Core
{
public interface IJobExecutorFactory
{
IJobExecutor? GetJobExecutor(string glueType);
}
}

@ -0,0 +1,12 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using System.Threading.Tasks;
namespace DotXxlJob.Core
{
public interface IJobLogger
{
Task LogAsync(string message);
}
}

@ -1,4 +1,4 @@
// Copyright (c) Xuanye Wong. All rights reserved.
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using System;

@ -0,0 +1,16 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using System;
using System.Threading.Tasks;
namespace DotXxlJob.Core.Internal
{
internal class CallbackWorker : IAsyncDisposable
{
public ValueTask DisposeAsync()
{
throw new NotImplementedException();
}
}
}

@ -0,0 +1,14 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using DotXxlJob.Core.Models;
namespace DotXxlJob.Core.Internal
{
internal class JobCompletedArgs
{
public Job Job { get; set; } = null!;
public TaskResult TaskResult { get; set; } = null!;
}
}

@ -0,0 +1,58 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using System;
using System.Threading;
using System.Threading.Tasks;
using DotXxlJob.Core.Models;
using Microsoft.Extensions.DependencyInjection;
namespace DotXxlJob.Core.Internal
{
public class JobDispatcher : IJobDispatcher
{
private readonly JobWorker _jobWorker;
private readonly CallbackWorker _callbackWorker;
public JobDispatcher(IServiceProvider provider)
{
_jobWorker = ActivatorUtilities.CreateInstance<JobWorker>(provider);
_jobWorker.JobCompleted += _jobWorker_JobCompleted;
_callbackWorker = ActivatorUtilities.CreateInstance<CallbackWorker>(provider);
}
private void _jobWorker_JobCompleted(object? sender, JobCompletedArgs e)
{
//TODO:add job callback task
throw new NotImplementedException();
}
public Task StartAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
var t1 = _jobWorker.DisposeAsync();
var t2 = _callbackWorker.DisposeAsync();
await Task.WhenAll(t1.AsTask(), t2.AsTask());
_jobWorker.JobCompleted -= _jobWorker_JobCompleted;
}
public Task AddJobAsync(Job job)
{
return _jobWorker.EnqueueJobAsync(job);
}
public Task ReplaceJobAsync(Job job)
{
return _jobWorker.ReplaceJobAsync(job);
}
public Task AbortJobAsync(JobId jobId)
{
return _jobWorker.AbortJobAsync(jobId);
}
}
}

@ -0,0 +1,26 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using System.Collections.Generic;
using System.Linq;
namespace DotXxlJob.Core.Internal
{
internal class JobExecutorFactory : IJobExecutorFactory
{
private readonly Dictionary<string, IJobExecutor> _jobExecutors;
public JobExecutorFactory(IEnumerable<IJobExecutor> jobExecutors)
{
_jobExecutors = jobExecutors.ToDictionary(x => x.GlueType);
}
public IJobExecutor? GetJobExecutor(string glueType)
{
if (_jobExecutors.TryGetValue(glueType, out var executor))
{
return executor;
}
return null;
}
}
}

@ -0,0 +1,134 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using DotXxlJob.Core.Models;
namespace DotXxlJob.Core.Internal
{
internal class JobQueue
{
private readonly ConcurrentQueue<Job> _jobs = new ConcurrentQueue<Job>();
private readonly IJobLogger _jobLogger;
private CancellationTokenSource? _cancellationTokenSource;
private Task? _runningTask;
public JobQueue(IJobExecutor executor, IJobLogger jobLogger)
{
Executor = executor;
_jobLogger = jobLogger;
}
public IJobExecutor Executor { get; private set; } = null!;
public event EventHandler<JobCompletedArgs>? OnJobTaskCompleted;
public async Task SetExecutor(IJobExecutor executor)
{
Executor = executor;
//TODO: cancel all pending jobs
await Stop();
_jobs.Clear();
}
public void EnqueueJob(Job job)
{
_jobs.Enqueue(job);
_runningTask = Start();
}
public bool IsRunning()
{
return _cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested;
}
public async Task Replace(Job job)
{
_jobs.Clear();
await Stop();
EnqueueJob(job);
}
private Task Start()
{
if (_cancellationTokenSource != null)
{
return _runningTask!;
}
_cancellationTokenSource = new CancellationTokenSource();
var ct = _cancellationTokenSource.Token;
return Task.Factory.StartNew(async () =>
{
while (!ct.IsCancellationRequested)
{
if (_jobs.IsEmpty)
{
//_logger.LogInformation("task queue is empty!");
break;
}
TaskResult? result = null;
Job? jobTask = null;
try
{
if (_jobs.TryDequeue(out jobTask))
{
//TODO: set Logger;
//_jobLogger.SetLogFile(jobTask.LogDateTime, jobTask.LogId);
//_jobLogger.Log("<br>----------- xxl-job job execute start -----------<br>----------- Param:{0}", jobTask.ExecutorParams);
var exectorToken = ct;
CancellationTokenSource? timeoutCts = null;
if (jobTask.ExecutorTimeout > 0)
{
timeoutCts = new CancellationTokenSource(jobTask.ExecutorTimeout * 1000);
exectorToken = CancellationTokenSource.CreateLinkedTokenSource(exectorToken, timeoutCts.Token).Token;
}
result = await Executor.ExecuteAsync(new JobExecuteContext(_jobLogger) { }, jobTask!, exectorToken);
if (timeoutCts != null && timeoutCts.IsCancellationRequested)
{
result = TaskResult.Timeout();
timeoutCts.Dispose();
timeoutCts = null;
}
//_jobLogger.Log("<br>----------- xxl-job job execute end(finish) -----------<br>----------- ReturnT:" + result.Code);
}
}
catch (Exception ex)
{
result = TaskResult.Failure("Dequeue Task Failed:" + ex.Message);
//_jobLogger.Log("<br>----------- JobThread Exception:" + ex.Message + "<br>----------- xxl-job job execute end(error) -----------");
}
if (jobTask != null)
{
OnJobTaskCompleted?.Invoke(this, new JobCompletedArgs() { Job = jobTask, TaskResult = result! });
}
}
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}, _cancellationTokenSource.Token);
}
private Task Stop()
{
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
return _runningTask!;
}
}
}

@ -0,0 +1,97 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using DotXxlJob.Core.Models;
using ErrorOr;
namespace DotXxlJob.Core.Internal
{
internal class JobWorker : IAsyncDisposable
{
private readonly IJobExecutorFactory _executorFactory;
private readonly ConcurrentDictionary<JobId, JobQueue> _jobsQueue = new ConcurrentDictionary<JobId, JobQueue>();
public JobWorker(IJobExecutorFactory executorFactory)
{
_executorFactory = executorFactory;
}
public event EventHandler<JobCompletedArgs>? JobCompleted;
private void OnJobTaskCompleted(object? sender, JobCompletedArgs args)
{
JobCompleted?.Invoke(this, args);
}
public async Task<ErrorOr<TaskResult>> EnqueueJobAsync(Job job)
{
var executor = _executorFactory.GetJobExecutor(job.GlueType);
if (executor == null)
{
return TaskResult.Failure("Executor not found");
}
//new job serial
if (!_jobsQueue.TryGetValue(job.JobId, out var queue))
{
queue = new JobQueue(executor);
queue.OnJobTaskCompleted += OnJobTaskCompleted;
queue.EnqueueJob(job);
if (_jobsQueue.TryAdd(job.JobId, queue))
{
return TaskResult.Success();
}
return TaskResult.Failure("add running queue executor error");
}
//change executor
if (queue.Executor != executor)
{
queue.SetExecutor(executor);
queue.EnqueueJob(job);
return TaskResult.Success();
}
//丢弃后续的
if (job.ExecutorBlockStrategy == ExecutorBlockStrategy.DISCARD_LATER.ToString())
{
//存在还没执行完成的任务
if (queue.IsRunning())
{
return TaskResult.Failure($"block strategy effect:{job.ExecutorBlockStrategy}");
}
}
else if (job.ExecutorBlockStrategy == ExecutorBlockStrategy.COVER_EARLY.ToString()) //覆盖较早的
{
return queue.Replace(job);
}
queue.EnqueueJob(job);
return TaskResult.Success();
}
public Task ReplaceJobAsync(Job job)
{
throw new NotImplementedException();
}
public Task AbortJobAsync(JobId jobId)
{
throw new NotImplementedException();
}
public ValueTask DisposeAsync()
{
throw new NotImplementedException();
}
}
}

@ -1,4 +1,4 @@
// Copyright (c) Xuanye Wong. All rights reserved.
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using System;

@ -0,0 +1,15 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
namespace DotXxlJob.Core
{
public class JobExecuteContext
{
public JobExecuteContext(IJobLogger jobLogger)
{
JobLogger = jobLogger;
}
public IJobLogger JobLogger { get; } = null!;
}
}

@ -0,0 +1,42 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using Microsoft.AspNetCore.Http;
using System.Runtime.Serialization;
namespace DotXxlJob.Core.Models
{
public class ApiResult
{
protected const int SUCCESS_CODE = StatusCodes.Status200OK;
protected const int FAILURE_CODE = StatusCodes.Status500InternalServerError;
protected const int TIMEOUT_CODE = StatusCodes.Status502BadGateway;
[DataMember(Name = "code", Order = 1)]
public int Code { get; set; }
[DataMember(Name = "msg", Order = 2)]
public string? Message { get; set; }
public static ApiResult Success(string message = "")
{
return new ApiResult() { Code = SUCCESS_CODE, Message = message };
}
public static ApiResult Failure(string message)
{
return new ApiResult() { Code = FAILURE_CODE, Message = message };
}
}
public class ApiResult<T> : ApiResult where T : class
{
[DataMember(Name = "content", Order = 3)]
public T Data { get; set; } = default!;
public static ApiResult Success(string message, T data)
{
return new ApiResult<T>() { Code = SUCCESS_CODE, Message = message, Data = data };
}
}
}

@ -0,0 +1,12 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
namespace DotXxlJob.Core.Models
{
public enum ExecutorBlockStrategy
{
SERIAL_EXECUTION,
DISCARD_LATER,
COVER_EARLY
}
}

@ -1,40 +0,0 @@
// Copyright (c) Xuanye Wong. All rights reserved.
// Licensed under MIT license
using System.Runtime.Serialization;
namespace DotXxlJob.Core.Models
{
public class ExecutorResult
{
protected const int SUCCESS_CODE = 200;
protected const int FAILURE_CODE = 500;
protected const int TIMEOUT_CODE = 502;
[DataMember(Name = "code",Order = 1)]
public int Code { get; set; }
[DataMember(Name = "msg",Order = 2)]
public string? Message { get; set; }
public static ExecutorResult Success(string message="")
{
return new ExecutorResult(){Code = SUCCESS_CODE, Message = message};
}
public static ExecutorResult Failure(string message)
{
return new ExecutorResult(){Code = FAILURE_CODE, Message = message};
}
}
public class ExecutorResult<T> : ExecutorResult where T : class
{
[DataMember(Name = "content",Order = 3)]
public T Data { get; set; } = default!;
public static ExecutorResult Success(string message, T data)
{
return new ExecutorResult<T>(){Code = SUCCESS_CODE, Message = message,Data = data};
}
}
}

@ -1,4 +1,4 @@
// Copyright (c) Xuanye Wong. All rights reserved.
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using System.Runtime.Serialization;

@ -0,0 +1,22 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
namespace DotXxlJob.Core.Models
{
public readonly record struct JobId(int Value);
public class Job
{
public JobId JobId { get; set; }
public string GlueType { get; set; } = null!;
public string HandlerName { get; set; } = null!;
public string ExecutorParams { get; set; } = null!;
public IJobExecutor? Executor { get; set; }
public string ExecutorBlockStrategy { get; set; } = null!;
public int ExecutorTimeout { get; set; }
}
}

@ -1,4 +1,4 @@
// Copyright (c) Xuanye Wong. All rights reserved.
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using System.Runtime.Serialization;

@ -0,0 +1,41 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
namespace DotXxlJob.Core.Models
{
public enum ResultCodes
{
Success = 200,
Failure = 500,
Timeout = 502
}
public class TaskResult
{
public ResultCodes Code { get; set; }
public string? Message { get; set; }
private static TaskResult _successResult = new() { Code = ResultCodes.Success };
private static TaskResult _timeoutResult = new() { Code = ResultCodes.Timeout };
public static TaskResult Failure(string message)
{
return new TaskResult() { Code = ResultCodes.Failure, Message = message };
}
public static TaskResult Timeout()
{
return _timeoutResult;
}
public static TaskResult Success()
{
return _successResult;
}
}
internal class TaskResult<T> : TaskResult where T : class
{
public T Data { get; set; } = default!;
}
}

@ -1,4 +1,4 @@
// Copyright (c) Xuanye Wong. All rights reserved.
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using System.Runtime.Serialization;

@ -0,0 +1,12 @@
{
"profiles": {
"DotXxlJob.Core": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:51910;http://localhost:51911"
}
}
}

@ -0,0 +1,67 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using System;
using System.IO;
namespace DotXxlJob.Core
{
public class XxlJobExecutorOptions
{
/// <summary>
/// admin site url, separated by semicolons
/// </summary>
public string AdminSiteUrl { get; set; } = null!;
/// <summary>
/// App name, must be consistent with the configuration on the admin side when auto-registering
/// </summary>
public string AppName { get; set; } = "xxl-job-executor-dotnet";
/// <summary>
/// Special URL to bind, if this is configured, SpecialBindAddress and Port will be ignored
/// </summary>
public string? SpecialBindUrl { get; set; }
/// <summary>
/// Address to submit when auto-registering, if empty, the internal network address will be automatically obtained
/// </summary>
public string? SpecialBindAddress { get; set; }
/// <summary>
/// Port to bind
/// </summary>
public int Port { get; set; }
/// <summary>
/// Whether to auto-register
/// </summary>
public bool AutoRegistry { get; set; }
/// <summary>
/// Access token
/// </summary>
public string? AccessToken { get; set; }
/// <summary>
/// Log directory, defaults to the logs subdirectory of the execution directory, please configure an absolute path
/// </summary>
public string LogPath { get; set; } = Path.Combine(AppContext.BaseDirectory, "./logs");
/// <summary>
/// Log retention days
/// </summary>
public int LogRetentionDays { get; set; } = 30;
/// <summary>
/// Callback interval in milliseconds
/// </summary>
public int CallBackInterval { get; set; } = 500;
}
}

@ -0,0 +1,111 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using DotXxlJob.Core.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using System;
using System.IO;
using System.Threading.Tasks;
namespace DotXxlJob.Core
{
public class XxlJobHttpHandler
{
private readonly ICommandExecutorFactory _commandExecutorFactory;
private readonly ISerializer _serializer;
private readonly XxlJobExecutorOptions _options;
public XxlJobHttpHandler(ICommandExecutorFactory commandExecutorFactory, ISerializer serializer, IOptions<XxlJobExecutorOptions> optionsAccessor)
{
if (optionsAccessor?.Value == null)
{
throw new ArgumentNullException(nameof(optionsAccessor));
}
_options = optionsAccessor.Value;
_commandExecutorFactory = commandExecutorFactory;
_serializer = serializer;
}
public async Task HandleAsync(HttpContext context)
{
var request = context.Request;
var response = context.Response;
var command = GetMethodName(request.Path.Value);
if (string.IsNullOrEmpty(command))
{
//no need to do anything
return;
}
if (!ValidateAccessToken(request))
{
await SendErrorResponse(response, StatusCodes.Status401Unauthorized, "Unauthorized");
return;
}
var executor = _commandExecutorFactory.GetCommandExecutor(command);
if (executor == null)
{
await SendErrorResponse(response, StatusCodes.Status400BadRequest, "The method have not been implemented");
return;
}
byte[] payload;
using (var memoryStream = new MemoryStream())
{
await request.Body.CopyToAsync(memoryStream);
payload = memoryStream.ToArray();
}
var result = await executor.ExecuteAsync(payload);
await SendResponse(response, result.Code, result);
}
private bool ValidateAccessToken(HttpRequest request)
{
if (string.IsNullOrEmpty(_options.AccessToken))
{
return true;
}
if (request.Headers.TryGetValue("XXL-JOB-ACCESS-TOKEN", out var accessToken) && _options.AccessToken.Equals(accessToken))
{
return true;
}
return false;
}
private static string GetMethodName(string? path)
{
if (string.IsNullOrEmpty(path))
{
return string.Empty;
}
var arrParts = path.Split('/');
if (arrParts.Length < 1)
{
return string.Empty;
}
return arrParts[arrParts.Length - 1].ToLower();
}
private Task SendResponse(HttpResponse response, int statusCode, object? data = null)
{
response.StatusCode = statusCode;
response.ContentType = "application/json";
if (data != null)
{
var bytes = _serializer.Serialize(data, data.GetType());
return response.Body.WriteAsync(bytes, 0, bytes.Length);
}
return Task.CompletedTask;
}
private Task SendErrorResponse(HttpResponse response, int statusCode, string message)
{
return SendResponse(response, statusCode, ApiResult.Failure(message));
}
}
}

@ -0,0 +1,44 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using DotXxlJob.Core.CommandExecutors;
using Moq;
namespace DotXxlJob.Core.UnitTests
{
public class CommandExecutorFactoryTests
{
[Fact]
public void GetCommandExecutor_ReturnsExecutor_WhenExecutorExists()
{
// Arrange
var mockExecutor = new Mock<ICommandExecutor>();
mockExecutor.Setup(e => e.CommandName).Returns("TestCommand");
var executors = new List<ICommandExecutor> { mockExecutor.Object };
var factory = new CommandExecutorFactory(executors);
// Act
var result = factory.GetCommandExecutor("TestCommand");
// Assert
Assert.NotNull(result);
Assert.Equal("TestCommand", result?.CommandName);
}
[Fact]
public void GetCommandExecutor_ReturnsNull_WhenExecutorDoesNotExist()
{
// Arrange
var mockExecutor = new Mock<ICommandExecutor>();
mockExecutor.Setup(e => e.CommandName).Returns("TestCommand");
var executors = new List<ICommandExecutor> { mockExecutor.Object };
var factory = new CommandExecutorFactory(executors);
// Act
var result = factory.GetCommandExecutor("NonExistentCommand");
// Assert
Assert.Null(result);
}
}
}

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\DotXxlJob.Core\DotXxlJob.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>

@ -0,0 +1,84 @@
// Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license
using DotXxlJob.Core.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Moq;
using System.Text;
namespace DotXxlJob.Core.UnitTests
{
public class XxlJobHttpHandlerTests
{
private readonly Mock<ICommandExecutorFactory> _commandExecutorFactoryMock;
private readonly Mock<ISerializer> _serializerMock;
private readonly Mock<IOptions<XxlJobExecutorOptions>> _optionsAccessorMock;
private readonly XxlJobHttpHandler _handler;
public XxlJobHttpHandlerTests()
{
_commandExecutorFactoryMock = new Mock<ICommandExecutorFactory>();
_serializerMock = new Mock<ISerializer>();
_optionsAccessorMock = new Mock<IOptions<XxlJobExecutorOptions>>();
_optionsAccessorMock.Setup(o => o.Value).Returns(new XxlJobExecutorOptions { AccessToken = "test-token" });
_handler = new XxlJobHttpHandler(_commandExecutorFactoryMock.Object, _serializerMock.Object, _optionsAccessorMock.Object);
}
[Fact]
public async Task HandleAsync_ShouldReturnUnauthorized_WhenAccessTokenIsInvalid()
{
// Arrange
var context = new DefaultHttpContext();
context.Request.Path = "/test-command";
context.Request.Headers["XXL-JOB-ACCESS-TOKEN"] = "invalid-token";
// Act
await _handler.HandleAsync(context);
// Assert
Assert.Equal(StatusCodes.Status401Unauthorized, context.Response.StatusCode);
}
[Fact]
public async Task HandleAsync_ShouldReturnBadRequest_WhenCommandExecutorIsNull()
{
// Arrange
var context = new DefaultHttpContext();
context.Request.Path = "/test-command";
context.Request.Headers["XXL-JOB-ACCESS-TOKEN"] = "test-token";
_commandExecutorFactoryMock.Setup(f => f.GetCommandExecutor(It.IsAny<string>())).Returns((ICommandExecutor)null);
// Act
await _handler.HandleAsync(context);
// Assert
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
}
[Fact]
public async Task HandleAsync_ShouldReturnSuccess_WhenCommandExecutorExecutesSuccessfully()
{
// Arrange
var context = new DefaultHttpContext();
context.Request.Path = "/test-command";
context.Request.Headers["XXL-JOB-ACCESS-TOKEN"] = "test-token";
context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("test-payload"));
var executorMock = new Mock<ICommandExecutor>();
executorMock.Setup(e => e.ExecuteAsync(It.IsAny<byte[]>())).ReturnsAsync(new ApiResult { Code = 200 });
_commandExecutorFactoryMock.Setup(f => f.GetCommandExecutor(It.IsAny<string>())).Returns(executorMock.Object);
// Act
await _handler.HandleAsync(context);
// Assert
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
}
}
}
Loading…
Cancel
Save