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. 196
      .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. 9
      src/DotXxlJob.Core/CommandExecutors/BeatCommandExecutor.cs
  11. 28
      src/DotXxlJob.Core/CommandExecutors/CommandExecutorFactory.cs
  12. 11
      src/DotXxlJob.Core/CommandExecutors/IdleBeatCommandExecutor.cs
  13. 9
      src/DotXxlJob.Core/CommandExecutors/KillCommandExecutor.cs
  14. 13
      src/DotXxlJob.Core/CommandExecutors/TriggerCommandExecutor.cs
  15. 2
      src/DotXxlJob.Core/DependencyExtensions.cs
  16. 5
      src/DotXxlJob.Core/DotXxlJob.Core.csproj
  17. 7
      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. 6
      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. 12
      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. 44
      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

@ -1,100 +1,100 @@
# Rules in this file were initially inferred by Visual Studio IntelliCode from the C:\workspaces\opensource\uuac codebase based on best match to current usage at 1/18/2022 # Rules in this file were initially inferred by Visual Studio IntelliCode from the C:\workspaces\opensource\uuac codebase based on best match to current usage at 1/18/2022
# There already existed an .editorconfig file in this directory. Copy rules from this .editorconfig.inferred file to the existing .editorconfig file as desired to have them take effect at this location. # There already existed an .editorconfig file in this directory. Copy rules from this .editorconfig.inferred file to the existing .editorconfig file as desired to have them take effect at this location.
# You can modify the rules from these initially generated values to suit your own policies # You can modify the rules from these initially generated values to suit your own policies
# You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference # You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference
[*.cs] [*.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 dotnet_diagnostic.IDE0073.severity = warning
#Core editorconfig formatting - indentation #Core editorconfig formatting - indentation
#use soft tabs (spaces) for indentation #use soft tabs (spaces) for indentation
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
#Formatting - new line options #Formatting - new line options
#require members of object intializers to be on separate lines #require members of object intializers to be on separate lines
csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_object_initializers = true
#require braces to be on a new line for lambdas, types, control_blocks, and methods (also known as "Allman" style) #require braces to be on a new line for lambdas, types, control_blocks, and methods (also known as "Allman" style)
csharp_new_line_before_open_brace = all csharp_new_line_before_open_brace = all
#Formatting - organize using options #Formatting - organize using options
#do not place System.* using directives before other using directives #do place System.* using directives before other using directives
dotnet_sort_system_directives_first = false dotnet_sort_system_directives_first = true
#Formatting - spacing options #Formatting - spacing options
#require NO space between a cast and the value #require NO space between a cast and the value
csharp_space_after_cast = false csharp_space_after_cast = false
#require a space before the colon for bases or interfaces in a type declaration #require a space before the colon for bases or interfaces in a type declaration
csharp_space_after_colon_in_inheritance_clause = true csharp_space_after_colon_in_inheritance_clause = true
#require a space after a keyword in a control flow statement such as a for loop #require a space after a keyword in a control flow statement such as a for loop
csharp_space_after_keywords_in_control_flow_statements = true csharp_space_after_keywords_in_control_flow_statements = true
#require a space before the colon for bases or interfaces in a type declaration #require a space before the colon for bases or interfaces in a type declaration
csharp_space_before_colon_in_inheritance_clause = true csharp_space_before_colon_in_inheritance_clause = true
#remove space within empty argument list parentheses #remove space within empty argument list parentheses
csharp_space_between_method_call_empty_parameter_list_parentheses = false csharp_space_between_method_call_empty_parameter_list_parentheses = false
#remove space between method call name and opening parenthesis #remove space between method call name and opening parenthesis
csharp_space_between_method_call_name_and_opening_parenthesis = false csharp_space_between_method_call_name_and_opening_parenthesis = false
#do not place space characters after the opening parenthesis and before the closing parenthesis of a method call #do not place space characters after the opening parenthesis and before the closing parenthesis of a method call
csharp_space_between_method_call_parameter_list_parentheses = false csharp_space_between_method_call_parameter_list_parentheses = false
#place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list. #place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list.
csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_method_declaration_parameter_list_parentheses = false
#Formatting - wrapping options #Formatting - wrapping options
#leave code block on separate lines #leave code block on separate lines
csharp_preserve_single_line_blocks = true csharp_preserve_single_line_blocks = true
#Style - Code block preferences #Style - Code block preferences
#prefer curly braces even for one line of code #prefer curly braces even for one line of code
csharp_prefer_braces = when_multiline:suggestion csharp_prefer_braces = when_multiline:suggestion
#Style - expression bodied member options #Style - expression bodied member options
#prefer block bodies for accessors #prefer block bodies for accessors
csharp_style_expression_bodied_accessors = false:suggestion csharp_style_expression_bodied_accessors = false:suggestion
#prefer block bodies for constructors #prefer block bodies for constructors
csharp_style_expression_bodied_constructors = false:suggestion csharp_style_expression_bodied_constructors = false:suggestion
#prefer block bodies for methods #prefer block bodies for methods
csharp_style_expression_bodied_methods = false:suggestion csharp_style_expression_bodied_methods = false:suggestion
#prefer block bodies for properties #prefer block bodies for properties
csharp_style_expression_bodied_properties = true:suggestion csharp_style_expression_bodied_properties = true:suggestion
#Style - Expression-level preferences #Style - Expression-level preferences
#prefer objects to be initialized using object initializers when possible #prefer objects to be initialized using object initializers when possible
dotnet_style_object_initializer = true:suggestion dotnet_style_object_initializer = true:suggestion
#Style - implicit and explicit types #Style - implicit and explicit types
#prefer var over explicit type in all cases, unless overridden by another code style rule #prefer var over explicit type in all cases, unless overridden by another code style rule
csharp_style_var_elsewhere = true:suggestion csharp_style_var_elsewhere = true:suggestion
#prefer var when the type is already mentioned on the right-hand side of a declaration expression #prefer var when the type is already mentioned on the right-hand side of a declaration expression
csharp_style_var_when_type_is_apparent = true:suggestion csharp_style_var_when_type_is_apparent = true:suggestion
#Style - language keyword and framework type options #Style - language keyword and framework type options
#prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them #prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
#Style - modifier options #Style - modifier options
#prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods. #prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods.
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
#Style - Modifier preferences #Style - Modifier preferences
#when this rule is set to a list of modifiers, prefer the specified ordering. #when this rule is set to a list of modifiers, prefer the specified ordering.
csharp_preferred_modifier_order = public,private,static,readonly:suggestion csharp_preferred_modifier_order = public,private,static,readonly:suggestion
#Style - qualification options #Style - qualification options
#prefer fields not to be prefaced with this. or Me. in Visual Basic #prefer fields not to be prefaced with this. or Me. in Visual Basic
dotnet_style_qualification_for_field = false:suggestion dotnet_style_qualification_for_field = false:suggestion
#prefer properties not to be prefaced with this. or Me. in Visual Basic #prefer properties not to be prefaced with this. or Me. in Visual Basic
dotnet_style_qualification_for_property = false:suggestion dotnet_style_qualification_for_property = false:suggestion
csharp_indent_labels = one_less_than_current csharp_indent_labels = one_less_than_current
csharp_using_directive_placement = outside_namespace:silent csharp_using_directive_placement = outside_namespace:silent
@ -132,7 +132,7 @@ csharp_style_prefer_extended_property_pattern = true:suggestion
csharp_style_prefer_not_pattern = true:suggestion csharp_style_prefer_not_pattern = true:suggestion
csharp_style_var_for_built_in_types = false:silent csharp_style_var_for_built_in_types = false:silent
csharp_space_around_binary_operators = before_and_after csharp_space_around_binary_operators = before_and_after
csharp_style_prefer_readonly_struct = true:suggestion csharp_style_prefer_readonly_struct = true:suggestion
[*.{cs,vb}] [*.{cs,vb}]
#### Naming styles #### #### Naming styles ####

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

@ -1,6 +1,6 @@
MIT License 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal 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的执行器实现 注意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,19 +1,18 @@
// Copyright (c) Xuanye Wong. All rights reserved. // Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license // Licensed under MIT license
using DotXxlJob.Core.Models; using DotXxlJob.Core.Models;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace DotXxlJob.Core.CommandExecutors namespace DotXxlJob.Core.CommandExecutors
{ {
public class BeatCommandExecutor:ICommandExecutor public class BeatCommandExecutor : ICommandExecutor
{ {
public string CommandName => "Beat"; 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,13 +1,12 @@
// Copyright (c) Xuanye Wong. All rights reserved. // Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license // Licensed under MIT license
using DotXxlJob.Core.Models; using DotXxlJob.Core.Models;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace DotXxlJob.Core.CommandExecutors namespace DotXxlJob.Core.CommandExecutors
{ {
public class IdleBeatCommandExecutor:ICommandExecutor public class IdleBeatCommandExecutor : ICommandExecutor
{ {
private readonly ISerializer _serializer; private readonly ISerializer _serializer;
@ -16,13 +15,13 @@ namespace DotXxlJob.Core.CommandExecutors
_serializer = serializer; _serializer = serializer;
} }
public string CommandName => "IdleBeat"; 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); var idleBeat = _serializer.Deserialize<IdleBeatCommand>(payload);
if (idleBeat == null) 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(); throw new System.NotImplementedException();
} }

@ -1,13 +1,12 @@
// Copyright (c) Xuanye Wong. All rights reserved. // Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license // Licensed under MIT license
using DotXxlJob.Core.Models; using DotXxlJob.Core.Models;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace DotXxlJob.Core.CommandExecutors namespace DotXxlJob.Core.CommandExecutors
{ {
public class KillCommandExecutor: ICommandExecutor public class KillCommandExecutor : ICommandExecutor
{ {
private readonly ISerializer _serializer; private readonly ISerializer _serializer;
@ -16,12 +15,12 @@ namespace DotXxlJob.Core.CommandExecutors
_serializer = serializer; _serializer = serializer;
} }
public string CommandName => "kill"; public string CommandName => "kill";
public Task<ExecutorResult> ExecuteAsync(byte[] payload, CancellationToken cancellationToken) public Task<ApiResult> ExecuteAsync(byte[] payload)
{ {
var command = _serializer.Deserialize<IdleBeatCommand>(payload); var command = _serializer.Deserialize<IdleBeatCommand>(payload);
if (command == null) 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(); throw new System.NotImplementedException();
} }

@ -1,13 +1,12 @@
// Copyright (c) Xuanye Wong. All rights reserved. // Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license // Licensed under MIT license
using DotXxlJob.Core.Models;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using DotXxlJob.Core.Models;
namespace DotXxlJob.Core.CommandExecutors namespace DotXxlJob.Core.CommandExecutors
{ {
public class TriggerCommandExecutor: ICommandExecutor public class TriggerCommandExecutor : ICommandExecutor
{ {
private readonly ISerializer _serializer; private readonly ISerializer _serializer;
@ -16,12 +15,12 @@ namespace DotXxlJob.Core.CommandExecutors
_serializer = serializer; _serializer = serializer;
} }
public string CommandName => "Run"; public string CommandName => "Run";
public Task<ExecutorResult> ExecuteAsync(byte[] payload, CancellationToken cancellationToken) public Task<ApiResult> ExecuteAsync(byte[] payload)
{ {
var command = _serializer.Deserialize<TriggerCommand>(payload); var command = _serializer.Deserialize<TriggerCommand>(payload);
if (command== null) 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(); 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 // Licensed under MIT license
namespace DotXxlJob.Core namespace DotXxlJob.Core

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

@ -1,8 +1,7 @@
// Copyright (c) Xuanye Wong. All rights reserved. // Copyright (c) Xuanye Wang. All rights reserved.
// Licensed under MIT license // Licensed under MIT license
using DotXxlJob.Core.Models; using DotXxlJob.Core.Models;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace DotXxlJob.Core namespace DotXxlJob.Core
@ -10,8 +9,8 @@ namespace DotXxlJob.Core
public interface ICommandExecutor public interface ICommandExecutor
{ {
string CommandName { get; } 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 // Licensed under MIT license
namespace DotXxlJob.Core namespace DotXxlJob.Core
{ {
public interface ICommandExecutorFactory 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 // Licensed under MIT license
using System; using System;
@ -12,7 +12,7 @@ namespace DotXxlJob.Core
{ {
T? Deserialize<T>(byte[] data) where T : class; T? Deserialize<T>(byte[] data) where T : class;
byte[] Serialize<T>(T item) where T : class; byte[] Serialize<T>(T item) where T : class;
object? Deserialize(byte[] data,Type type); object? Deserialize(byte[] data, Type type);
byte[] Serialize(object item,Type type); byte[] Serialize(object item, Type type);
} }
} }

@ -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 // Licensed under MIT license
using System; using System;
@ -10,7 +10,7 @@ using System.Threading.Tasks;
namespace DotXxlJob.Core.Internal namespace DotXxlJob.Core.Internal
{ {
public class TextJsonSerializer:ISerializer public class TextJsonSerializer : ISerializer
{ {
public T? Deserialize<T>(byte[] data) where T : class public T? Deserialize<T>(byte[] data) where T : class
{ {
@ -26,14 +26,14 @@ namespace DotXxlJob.Core.Internal
public object? Deserialize(byte[] data, Type type) public object? Deserialize(byte[] data, Type type)
{ {
var json = Encoding.UTF8.GetString(data); var json = Encoding.UTF8.GetString(data);
return JsonSerializer.Deserialize(json,type); return JsonSerializer.Deserialize(json, type);
} }
public byte[] Serialize(object item,Type type) public byte[] Serialize(object item, Type type)
{ {
return JsonSerializer.SerializeToUtf8Bytes(item); return JsonSerializer.SerializeToUtf8Bytes(item);
} }
} }
} }

@ -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 // Licensed under MIT license
using System.Runtime.Serialization; 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 // Licensed under MIT license
using System.Runtime.Serialization; 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 // Licensed under MIT license
using System.Runtime.Serialization; using System.Runtime.Serialization;
@ -15,32 +15,32 @@ namespace DotXxlJob.Core.Models
public string ExecutorHandler { get; set; } = null!; public string ExecutorHandler { get; set; } = null!;
[DataMember(Name = "executorParams", Order = 3)] [DataMember(Name = "executorParams", Order = 3)]
public string ExecutorParams{ get; set; } = null!; public string ExecutorParams { get; set; } = null!;
[DataMember(Name = "executorBlockStrategy", Order = 4)] [DataMember(Name = "executorBlockStrategy", Order = 4)]
public string ExecutorBlockStrategy{ get; set; }= null!; public string ExecutorBlockStrategy { get; set; } = null!;
[DataMember(Name = "executorTimeout", Order = 5)] [DataMember(Name = "executorTimeout", Order = 5)]
public int ExecutorTimeout{ get; set; } public int ExecutorTimeout { get; set; }
[DataMember(Name = "logId",Order = 5)] [DataMember(Name = "logId", Order = 5)]
public long LogId { get; set; } public long LogId { get; set; }
[DataMember(Name = "logDateTime", Order = 6)] [DataMember(Name = "logDateTime", Order = 6)]
public long LogDateTime{ get; set; } public long LogDateTime { get; set; }
[DataMember(Name = "glueType",Order = 7)] [DataMember(Name = "glueType", Order = 7)]
public string? GlueType{ get; set; } public string? GlueType { get; set; }
[DataMember(Name = "glueSource",Order = 8)] [DataMember(Name = "glueSource", Order = 8)]
public string? GlueSource{ get; set; } public string? GlueSource { get; set; }
[DataMember(Name = "glueUpdatetime", Order = 9)] [DataMember(Name = "glueUpdatetime", Order = 9)]
public long GlueUpdateTime{ get; set; } public long GlueUpdateTime { get; set; }
[DataMember(Name = "broadcastIndex",Order = 10)] [DataMember(Name = "broadcastIndex", Order = 10)]
public int BroadcastIndex{ get; set; } public int BroadcastIndex { get; set; }
[DataMember(Name = "broadcastTotal",Order = 11)] [DataMember(Name = "broadcastTotal", Order = 11)]
public int BroadcastTotal{ get; set; } public int BroadcastTotal { get; set; }
} }
} }

@ -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