diff --git a/.github/workflows/publish-beta.yml b/.github/workflows/publish-beta.yml
index 633a8ea..a746a37 100644
--- a/.github/workflows/publish-beta.yml
+++ b/.github/workflows/publish-beta.yml
@@ -6,27 +6,32 @@ on:
paths:
- 'AntJob/**'
- 'AntJob.Extensions/**'
+ pull_request:
+ branches: [ master ]
+ paths:
+ - 'AntJob/**'
+ - 'AntJob.Extensions/**'
workflow_dispatch:
jobs:
build-publish:
-
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - name: Setup .NET
- uses: actions/setup-dotnet@v2
+ - uses: actions/checkout@v4
+ - name: Setup dotNET
+ uses: actions/setup-dotnet@v3
with:
- dotnet-version: 6.0.x
- - name: Restore
- run: |
- dotnet restore AntJob/AntJob.csproj
- dotnet restore AntJob.Extensions/AntJob.Extensions.csproj
+ dotnet-version: |
+ 6.0.x
+ 7.0.x
+ 8.x
+ - name: Get Version
+ run: echo "VERSION=$(date '+%Y.%m%d-beta%H%M')" >> $GITHUB_ENV
- name: Build
run: |
- dotnet pack --no-restore --version-suffix $(date "+%Y.%m%d-beta%H%M") -c Release -o out AntJob/AntJob.csproj
- dotnet pack --no-restore --version-suffix $(date "+%Y.%m%d-beta%H%M") -c Release -o out AntJob.Extensions/AntJob.Extensions.csproj
+ dotnet pack --version-suffix ${{ env.VERSION }} -c Release -o out AntJob/AntJob.csproj
+ dotnet pack --version-suffix ${{ env.VERSION }} -c Release -o out AntJob.Extensions/AntJob.Extensions.csproj
- name: Publish
run: |
dotnet nuget push ./out/*.nupkg --skip-duplicate --source https://nuget.pkg.github.com/NewLifeX/index.json --api-key ${{ github.token }}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 6793eba..cb1d710 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -7,23 +7,25 @@ on:
jobs:
build-publish:
-
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - name: Setup .NET
- uses: actions/setup-dotnet@v2
+ - uses: actions/checkout@v4
+ - name: Setup dotNET
+ uses: actions/setup-dotnet@v3
with:
- dotnet-version: 6.0.x
- - name: Restore
- run: |
- dotnet restore AntJob/AntJob.csproj
- dotnet restore AntJob.Extensions/AntJob.Extensions.csproj
+ dotnet-version: |
+ 6.0.x
+ 7.0.x
+ 8.x
+ - name: Get Version
+ run: echo "VERSION=$(date '+%Y.%m%d')" >> $GITHUB_ENV
- name: Build
run: |
- dotnet pack --no-restore --version-suffix $(date "+%Y.%m%d") -c Release -o out AntJob/AntJob.csproj
- dotnet pack --no-restore --version-suffix $(date "+%Y.%m%d") -c Release -o out AntJob.Extensions/AntJob.Extensions.csproj
+ dotnet pack --version-suffix ${{ env.VERSION }} -c Release -o out AntJob/AntJob.csproj
+ dotnet pack --version-suffix ${{ env.VERSION }} -c Release -o out AntJob.Extensions/AntJob.Extensions.csproj
+ dotnet pack --version-suffix ${{ env.VERSION }} -c Release -o out AntJob.Data/AntJob.Data.csproj
+ dotnet pack --version-suffix ${{ env.VERSION }} -c Release -o out AntWeb/AntWeb.csproj
- name: Publish
run: |
dotnet nuget push ./out/*.nupkg --skip-duplicate --source https://nuget.pkg.github.com/NewLifeX/index.json --api-key ${{ github.token }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1ce515c..7708073 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -9,15 +9,17 @@ on:
jobs:
build-publish:
-
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - name: Setup .NET
- uses: actions/setup-dotnet@v2
+ - uses: actions/checkout@v4
+ - name: Setup dotNET
+ uses: actions/setup-dotnet@v3
with:
- dotnet-version: 6.0.x
+ dotnet-version: |
+ 6.0.x
+ 7.0.x
+ 8.x
- name: Build
run: |
dotnet build -c Release
diff --git "a/\350\232\202\350\232\201.sln" "b/\350\232\202\350\232\201.sln"
index c2c4ec0..f549049 100644
--- "a/\350\232\202\350\232\201.sln"
+++ "b/\350\232\202\350\232\201.sln"
@@ -5,6 +5,7 @@ VisualStudioVersion = 17.1.32328.378
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Others", "Others", "{0EA980BB-BB15-41A3-B75B-537BC42E985B}"
ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
.github\workflows\publish-beta.yml = .github\workflows\publish-beta.yml
.github\workflows\publish.yml = .github\workflows\publish.yml
Readme.MD = Readme.MD
@@ -25,11 +26,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AntJob.Extensions", "AntJob
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{E842807F-C45E-44DA-8AAE-7915C1EBF2A2}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A1EF271C-AEA8-4EA3-A76F-906B4D4A9058}"
- ProjectSection(SolutionItems) = preProject
- .editorconfig = .editorconfig
- EndProjectSection
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AntJob.Agent", "AntJob.Agent\AntJob.Agent.csproj", "{0970FDBA-2331-4600-8DD5-A37B41AF989F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HisAgent", "Samples\HisAgent\HisAgent.csproj", "{E62006DC-E61B-42B0-A06B-ED5BF3F73D9E}"
@@ -38,8 +34,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HisData", "Samples\HisData\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HisWeb", "Samples\HisWeb\HisWeb.csproj", "{153499A6-E73C-4C5A-8867-D29BD586A74B}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UnitTest", "UnitTest", "{26660D0A-8724-4434-88D1-5EE861A68309}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AntTest", "AntTest\AntTest.csproj", "{A13E58D2-A185-4945-98B7-B6B0951A19D2}"
EndProject
Global
@@ -100,7 +94,6 @@ Global
{E62006DC-E61B-42B0-A06B-ED5BF3F73D9E} = {E842807F-C45E-44DA-8AAE-7915C1EBF2A2}
{38F8667D-70B7-4A90-8CA7-63738E925DFF} = {E842807F-C45E-44DA-8AAE-7915C1EBF2A2}
{153499A6-E73C-4C5A-8867-D29BD586A74B} = {E842807F-C45E-44DA-8AAE-7915C1EBF2A2}
- {A13E58D2-A185-4945-98B7-B6B0951A19D2} = {26660D0A-8724-4434-88D1-5EE861A68309}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9337283C-C795-479F-A2F1-C892EBE2490C}
diff --git a/AntJob.Agent/AntJob.Agent.csproj b/AntJob.Agent/AntJob.Agent.csproj
index da8e8c4..347bbb9 100644
--- a/AntJob.Agent/AntJob.Agent.csproj
+++ b/AntJob.Agent/AntJob.Agent.csproj
@@ -32,7 +32,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="NewLife.Stardust" Version="2.9.2024.402" />
+ <PackageReference Include="NewLife.Stardust" Version="3.1.2024.1004" />
</ItemGroup>
<ItemGroup>
diff --git a/AntJob.Agent/Program.cs b/AntJob.Agent/Program.cs
index cebe280..4ecf648 100644
--- a/AntJob.Agent/Program.cs
+++ b/AntJob.Agent/Program.cs
@@ -1,6 +1,5 @@
using AntJob;
using AntJob.Extensions;
-using AntJob.Providers;
using NewLife.Log;
using NewLife.Model;
using Stardust;
@@ -17,18 +16,10 @@ var set = AntSetting.Current;
var scheduler = new Scheduler
{
ServiceProvider = services.BuildServiceProvider(),
-
- //// 使用分布式调度引擎替换默认的本地文件调度
- //Provider = new NetworkJobProvider
- //{
- // Debug = set.Debug,
- // Server = set.Server,
- // AppID = set.AppID,
- // Secret = set.Secret,
- //}
+ Log = XTrace.Log,
};
-scheduler.Join(set.Server, set.AppID, set.Secret, set.Debug);
+scheduler.Join(set);
// 添加作业处理器
//sc.Handlers.Add(new CSharpHandler());
@@ -36,7 +27,7 @@ scheduler.AddHandler<SqlHandler>();
scheduler.AddHandler<SqlMessage>();
// 启动调度引擎,调度器内部多线程处理
-scheduler.Start();
+scheduler.StartAsync();
// 友好退出
var host = services.BuildHost();
diff --git a/AntJob.Data/Ant.htm b/AntJob.Data/Ant.htm
index d0a79f9..a87cefb 100644
--- a/AntJob.Data/Ant.htm
+++ b/AntJob.Data/Ant.htm
@@ -68,7 +68,7 @@
<td></td>
<td title="唯一索引">UQ</td>
<td>N</td>
- <td></td>
+ <td>应用英文名</td>
</tr>
<tr>
@@ -79,7 +79,7 @@
<td></td>
<td></td>
<td></td>
- <td></td>
+ <td>应用中文名</td>
</tr>
<tr>
@@ -90,7 +90,7 @@
<td></td>
<td></td>
<td></td>
- <td></td>
+ <td>一般不设置,应用默认接入</td>
</tr>
<tr>
@@ -145,7 +145,7 @@
<td></td>
<td></td>
<td>N</td>
- <td></td>
+ <td>该应用下作业个数</td>
</tr>
<tr>
@@ -156,6 +156,28 @@
<td></td>
<td></td>
<td>N</td>
+ <td>该应用下消息条数</td>
+ </tr>
+
+ <tr>
+ <td>ManagerId</td>
+ <td>管理人</td>
+ <td>Int32</td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td>N</td>
+ <td>负责该应用的管理员</td>
+ </tr>
+
+ <tr>
+ <td>Manager</td>
+ <td>管理者</td>
+ <td>String</td>
+ <td>50</td>
+ <td></td>
+ <td></td>
+ <td></td>
<td></td>
</tr>
@@ -375,6 +397,17 @@
</tr>
<tr>
+ <td>Enable</td>
+ <td>启用</td>
+ <td>Boolean</td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td>N</td>
+ <td>是否允许申请任务</td>
+ </tr>
+
+ <tr>
<td>Tasks</td>
<td>任务数</td>
<td>Int32</td>
@@ -657,7 +690,7 @@
</tbody>
</table>
<br></br>
-<h3>应用配置(AppConfig)</h3>
+<h3>作业(Job)</h3>
<table>
<thead>
<tr>
@@ -698,7 +731,7 @@
<td>Name</td>
<td>名称</td>
<td>String</td>
- <td>50</td>
+ <td>100</td>
<td></td>
<td></td>
<td></td>
@@ -706,41 +739,19 @@
</tr>
<tr>
- <td>Content</td>
- <td>内容</td>
+ <td>ClassName</td>
+ <td>类名</td>
<td>String</td>
- <td>5000</td>
- <td></td>
- <td></td>
- <td></td>
- <td>一般是json格式</td>
- </tr>
-
- <tr>
- <td>CreateUserID</td>
- <td>创建人</td>
- <td>Int32</td>
- <td></td>
- <td></td>
- <td></td>
- <td>N</td>
- <td></td>
- </tr>
-
- <tr>
- <td>CreateTime</td>
- <td>创建时间</td>
- <td>DateTime</td>
- <td></td>
- <td></td>
+ <td>100</td>
<td></td>
<td></td>
<td></td>
+ <td>支持该作业的处理器实现</td>
</tr>
<tr>
- <td>CreateIP</td>
- <td>创建地址</td>
+ <td>DisplayName</td>
+ <td>显示名</td>
<td>String</td>
<td>50</td>
<td></td>
@@ -750,9 +761,9 @@
</tr>
<tr>
- <td>UpdateUserID</td>
- <td>更新人</td>
- <td>Int32</td>
+ <td>Enable</td>
+ <td>启用</td>
+ <td>Boolean</td>
<td></td>
<td></td>
<td></td>
@@ -761,108 +772,25 @@
</tr>
<tr>
- <td>UpdateTime</td>
- <td>更新时间</td>
- <td>DateTime</td>
- <td></td>
- <td></td>
- <td></td>
- <td></td>
- <td></td>
- </tr>
-
- <tr>
- <td>UpdateIP</td>
- <td>更新地址</td>
- <td>String</td>
- <td>50</td>
- <td></td>
- <td></td>
- <td></td>
- <td></td>
- </tr>
- </tbody>
-</table>
-<br></br>
-<h3>作业(Job)</h3>
-<table>
- <thead>
- <tr>
- <th>名称</th>
- <th>显示名</th>
- <th>类型</th>
- <th>长度</th>
- <th>精度</th>
- <th>主键</th>
- <th>允许空</th>
- <th>备注</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>ID</td>
- <td>编号</td>
- <td>Int32</td>
- <td></td>
- <td></td>
- <td title="自增">AI</td>
- <td>N</td>
- <td></td>
- </tr>
-
- <tr>
- <td>AppID</td>
- <td>应用</td>
+ <td>Mode</td>
+ <td>调度模式</td>
<td>Int32</td>
<td></td>
<td></td>
<td></td>
<td>N</td>
- <td></td>
- </tr>
-
- <tr>
- <td>Name</td>
- <td>名称</td>
- <td>String</td>
- <td>100</td>
- <td></td>
- <td></td>
- <td></td>
- <td></td>
- </tr>
-
- <tr>
- <td>ClassName</td>
- <td>类名</td>
- <td>String</td>
- <td>100</td>
- <td></td>
- <td></td>
- <td></td>
- <td>支持该作业的处理器实现</td>
+ <td>定时调度只要达到时间片开头就可以跑,数据调度要求达到时间片末尾才可以跑</td>
</tr>
<tr>
- <td>DisplayName</td>
- <td>显示名</td>
+ <td>Cron</td>
+ <td>执行频次</td>
<td>String</td>
<td>50</td>
<td></td>
<td></td>
<td></td>
- <td></td>
- </tr>
-
- <tr>
- <td>Mode</td>
- <td>调度模式</td>
- <td>Int32</td>
- <td></td>
- <td></td>
- <td></td>
- <td>N</td>
- <td>定时调度只要达到时间片开头就可以跑,数据调度要求达到时间片末尾才可以跑</td>
+ <td>定时调度的Cron表达式</td>
</tr>
<tr>
@@ -888,14 +816,14 @@
</tr>
<tr>
- <td>Start</td>
- <td>开始</td>
+ <td>DataTime</td>
+ <td>数据时间</td>
<td>DateTime</td>
<td></td>
<td></td>
<td></td>
<td></td>
- <td>大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用</td>
+ <td>下一次处理数据的时间,默认从当前时间开始</td>
</tr>
<tr>
@@ -906,7 +834,7 @@
<td></td>
<td></td>
<td></td>
- <td>小于不等于,数据调度到达该时间点后触发(可能有偏移量),默认空表示无止境,消息调度不适用</td>
+ <td>到该时间停止调度作业,默认不设置时永不停止</td>
</tr>
<tr>
@@ -994,7 +922,7 @@
<td></td>
<td></td>
<td>N</td>
- <td>任务项保留天数,超过天数的任务项将被删除,默认3天</td>
+ <td>任务项保留天数,超过天数的任务项将被删除,默认30天</td>
</tr>
<tr>
@@ -1020,6 +948,17 @@
</tr>
<tr>
+ <td>Deadline</td>
+ <td>最后期限</td>
+ <td>DateTime</td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td>超过该时间后,任务将不再执行</td>
+ </tr>
+
+ <tr>
<td>Total</td>
<td>总数</td>
<td>Int64</td>
@@ -1075,14 +1014,25 @@
</tr>
<tr>
- <td>Enable</td>
- <td>启用</td>
- <td>Boolean</td>
+ <td>LastStatus</td>
+ <td>最后状态</td>
+ <td>Int32</td>
<td></td>
<td></td>
<td></td>
<td>N</td>
+ <td>最后一次状态</td>
+ </tr>
+
+ <tr>
+ <td>LastTime</td>
+ <td>最后时间</td>
+ <td>DateTime</td>
+ <td></td>
+ <td></td>
<td></td>
+ <td></td>
+ <td>最后一次时间</td>
</tr>
<tr>
@@ -1257,8 +1207,8 @@
</tr>
<tr>
- <td>Start</td>
- <td>开始</td>
+ <td>DataTime</td>
+ <td>数据时间</td>
<td>DateTime</td>
<td></td>
<td></td>
@@ -1379,13 +1329,13 @@
<tr>
<td>MsgCount</td>
- <td>消费消息数</td>
+ <td>消息</td>
<td>Int32</td>
<td></td>
<td></td>
<td></td>
<td>N</td>
- <td></td>
+ <td>消费消息数</td>
</tr>
<tr>
@@ -1571,8 +1521,8 @@
</tr>
<tr>
- <td>Start</td>
- <td>开始</td>
+ <td>DataTime</td>
+ <td>数据时间</td>
<td>DateTime</td>
<td></td>
<td></td>
@@ -1764,6 +1714,17 @@
</tr>
<tr>
+ <td>DelayTime</td>
+ <td>延迟时间</td>
+ <td>DateTime</td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td>延迟到该时间执行</td>
+ </tr>
+
+ <tr>
<td>TraceId</td>
<td>追踪</td>
<td>String</td>
diff --git a/AntJob.Data/AntJob.Data.csproj b/AntJob.Data/AntJob.Data.csproj
index b6fbabe..c37bf5a 100644
--- a/AntJob.Data/AntJob.Data.csproj
+++ b/AntJob.Data/AntJob.Data.csproj
@@ -40,7 +40,7 @@
<None Remove="Build.tt" />
</ItemGroup>
<ItemGroup>
- <PackageReference Include="NewLife.XCode" Version="11.11.2024.402" />
+ <PackageReference Include="NewLife.XCode" Version="11.16.2024.1005" />
</ItemGroup>
<ItemGroup>
<None Update="Build.log">
diff --git "a/AntJob.Data/Entity/\345\272\224\347\224\250\345\216\206\345\217\262.Biz.cs" "b/AntJob.Data/Entity/\345\272\224\347\224\250\345\216\206\345\217\262.Biz.cs"
index f8c1af4..15d343d 100644
--- "a/AntJob.Data/Entity/\345\272\224\347\224\250\345\216\206\345\217\262.Biz.cs"
+++ "b/AntJob.Data/Entity/\345\272\224\347\224\250\345\216\206\345\217\262.Biz.cs"
@@ -13,6 +13,8 @@ public partial class AppHistory : EntityBase<AppHistory>
#region 对象操作
static AppHistory()
{
+ Meta.Table.DataTable.InsertOnly = true;
+
// 累加字段
//var df = Meta.Factory.AdditionalFields;
//df.Add(__.AppID);
diff --git "a/AntJob.Data/Entity/\345\272\224\347\224\250\345\216\206\345\217\262.cs" "b/AntJob.Data/Entity/\345\272\224\347\224\250\345\216\206\345\217\262.cs"
index 86b1436..2b1d236 100644
--- "a/AntJob.Data/Entity/\345\272\224\347\224\250\345\216\206\345\217\262.cs"
+++ "b/AntJob.Data/Entity/\345\272\224\347\224\250\345\216\206\345\217\262.cs"
@@ -27,7 +27,7 @@ public partial class AppHistory
[DisplayName("编号")]
[Description("编号")]
[DataObjectField(true, false, false, 0)]
- [BindColumn("Id", "编号", "")]
+ [BindColumn("Id", "编号", "", DataScale = "time")]
public Int64 Id { get => _Id; set { if (OnPropertyChanging("Id", value)) { _Id = value; OnPropertyChanged("Id"); } } }
private Int32 _AppID;
@@ -178,6 +178,20 @@ public partial class AppHistory
#endregion
+ #region 扩展查询
+ #endregion
+
+ #region 数据清理
+ /// <summary>清理指定时间段内的数据</summary>
+ /// <param name="start">开始时间。未指定时清理小于指定时间的所有数据</param>
+ /// <param name="end">结束时间</param>
+ /// <returns>清理行数</returns>
+ public static Int32 DeleteWith(DateTime start, DateTime end)
+ {
+ return Delete(_.Id.Between(start, end, Meta.Factory.Snow));
+ }
+ #endregion
+
#region 字段名
/// <summary>取得应用历史字段信息的快捷方式</summary>
public partial class _
diff --git "a/AntJob.Data/Entity/\345\272\224\347\224\250\347\263\273\347\273\237.Biz.cs" "b/AntJob.Data/Entity/\345\272\224\347\224\250\347\263\273\347\273\237.Biz.cs"
index b9eaf6b..d84bc6d 100644
--- "a/AntJob.Data/Entity/\345\272\224\347\224\250\347\263\273\347\273\237.Biz.cs"
+++ "b/AntJob.Data/Entity/\345\272\224\347\224\250\347\263\273\347\273\237.Biz.cs"
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Runtime.Serialization;
using System.Xml.Serialization;
using NewLife;
using NewLife.Data;
@@ -46,7 +47,7 @@ public partial class App : EntityBase<App>
#region 扩展属性
/// <summary>作业集合</summary>
- [XmlIgnore]
+ [XmlIgnore, IgnoreDataMember]
public IList<Job> Jobs => Extends.Get(nameof(Jobs), k => Job.FindAllByAppID(ID));
#endregion
diff --git "a/AntJob.Data/Entity/\345\272\224\347\224\250\347\263\273\347\273\237.cs" "b/AntJob.Data/Entity/\345\272\224\347\224\250\347\263\273\347\273\237.cs"
index 683427e..e19e79b 100644
--- "a/AntJob.Data/Entity/\345\272\224\347\224\250\347\263\273\347\273\237.cs"
+++ "b/AntJob.Data/Entity/\345\272\224\347\224\250\347\263\273\347\273\237.cs"
@@ -31,27 +31,27 @@ public partial class App
public Int32 ID { get => _ID; set { if (OnPropertyChanging("ID", value)) { _ID = value; OnPropertyChanged("ID"); } } }
private String _Name;
- /// <summary>名称</summary>
+ /// <summary>名称。应用英文名</summary>
[DisplayName("名称")]
- [Description("名称")]
+ [Description("名称。应用英文名")]
[DataObjectField(false, false, false, 50)]
- [BindColumn("Name", "名称", "", Master = true)]
+ [BindColumn("Name", "名称。应用英文名", "", Master = true)]
public String Name { get => _Name; set { if (OnPropertyChanging("Name", value)) { _Name = value; OnPropertyChanged("Name"); } } }
private String _DisplayName;
- /// <summary>显示名</summary>
+ /// <summary>显示名。应用中文名</summary>
[DisplayName("显示名")]
- [Description("显示名")]
+ [Description("显示名。应用中文名")]
[DataObjectField(false, false, true, 50)]
- [BindColumn("DisplayName", "显示名", "")]
+ [BindColumn("DisplayName", "显示名。应用中文名", "")]
public String DisplayName { get => _DisplayName; set { if (OnPropertyChanging("DisplayName", value)) { _DisplayName = value; OnPropertyChanged("DisplayName"); } } }
private String _Secret;
- /// <summary>密钥</summary>
+ /// <summary>密钥。一般不设置,应用默认接入</summary>
[DisplayName("密钥")]
- [Description("密钥")]
+ [Description("密钥。一般不设置,应用默认接入")]
[DataObjectField(false, false, true, 50)]
- [BindColumn("Secret", "密钥", "")]
+ [BindColumn("Secret", "密钥。一般不设置,应用默认接入", "")]
public String Secret { get => _Secret; set { if (OnPropertyChanging("Secret", value)) { _Secret = value; OnPropertyChanged("Secret"); } } }
private String _Category;
@@ -87,21 +87,37 @@ public partial class App
public Boolean Enable { get => _Enable; set { if (OnPropertyChanging("Enable", value)) { _Enable = value; OnPropertyChanged("Enable"); } } }
private Int32 _JobCount;
- /// <summary>作业数</summary>
+ /// <summary>作业数。该应用下作业个数</summary>
[DisplayName("作业数")]
- [Description("作业数")]
+ [Description("作业数。该应用下作业个数")]
[DataObjectField(false, false, false, 0)]
- [BindColumn("JobCount", "作业数", "")]
+ [BindColumn("JobCount", "作业数。该应用下作业个数", "")]
public Int32 JobCount { get => _JobCount; set { if (OnPropertyChanging("JobCount", value)) { _JobCount = value; OnPropertyChanged("JobCount"); } } }
private Int32 _MessageCount;
- /// <summary>消息数</summary>
+ /// <summary>消息数。该应用下消息条数</summary>
[DisplayName("消息数")]
- [Description("消息数")]
+ [Description("消息数。该应用下消息条数")]
[DataObjectField(false, false, false, 0)]
- [BindColumn("MessageCount", "消息数", "")]
+ [BindColumn("MessageCount", "消息数。该应用下消息条数", "")]
public Int32 MessageCount { get => _MessageCount; set { if (OnPropertyChanging("MessageCount", value)) { _MessageCount = value; OnPropertyChanged("MessageCount"); } } }
+ private Int32 _ManagerId;
+ /// <summary>管理人。负责该应用的管理员</summary>
+ [DisplayName("管理人")]
+ [Description("管理人。负责该应用的管理员")]
+ [DataObjectField(false, false, false, 0)]
+ [BindColumn("ManagerId", "管理人。负责该应用的管理员", "")]
+ public Int32 ManagerId { get => _ManagerId; set { if (OnPropertyChanging("ManagerId", value)) { _ManagerId = value; OnPropertyChanged("ManagerId"); } } }
+
+ private String _Manager;
+ /// <summary>管理者</summary>
+ [DisplayName("管理者")]
+ [Description("管理者")]
+ [DataObjectField(false, false, true, 50)]
+ [BindColumn("Manager", "管理者", "")]
+ public String Manager { get => _Manager; set { if (OnPropertyChanging("Manager", value)) { _Manager = value; OnPropertyChanged("Manager"); } } }
+
private Int32 _CreateUserID;
/// <summary>创建人</summary>
[Category("扩展")]
@@ -202,6 +218,8 @@ public partial class App
"Enable" => _Enable,
"JobCount" => _JobCount,
"MessageCount" => _MessageCount,
+ "ManagerId" => _ManagerId,
+ "Manager" => _Manager,
"CreateUserID" => _CreateUserID,
"CreateUser" => _CreateUser,
"CreateTime" => _CreateTime,
@@ -227,6 +245,8 @@ public partial class App
case "Enable": _Enable = value.ToBoolean(); break;
case "JobCount": _JobCount = value.ToInt(); break;
case "MessageCount": _MessageCount = value.ToInt(); break;
+ case "ManagerId": _ManagerId = value.ToInt(); break;
+ case "Manager": _Manager = Convert.ToString(value); break;
case "CreateUserID": _CreateUserID = value.ToInt(); break;
case "CreateUser": _CreateUser = Convert.ToString(value); break;
case "CreateTime": _CreateTime = value.ToDateTime(); break;
@@ -243,6 +263,17 @@ public partial class App
#endregion
#region 关联映射
+ /// <summary>管理人</summary>
+ [XmlIgnore, IgnoreDataMember, ScriptIgnore]
+ public XCode.Membership.User MyManager => Extends.Get(nameof(MyManager), k => XCode.Membership.User.FindByID(ManagerId));
+
+ /// <summary>管理人</summary>
+ [Map(nameof(ManagerId), typeof(XCode.Membership.User), "ID")]
+ public String ManagerName => MyManager?.Name;
+
+ #endregion
+
+ #region 扩展查询
#endregion
#region 字段名
@@ -252,13 +283,13 @@ public partial class App
/// <summary>编号</summary>
public static readonly Field ID = FindByName("ID");
- /// <summary>名称</summary>
+ /// <summary>名称。应用英文名</summary>
public static readonly Field Name = FindByName("Name");
- /// <summary>显示名</summary>
+ /// <summary>显示名。应用中文名</summary>
public static readonly Field DisplayName = FindByName("DisplayName");
- /// <summary>密钥</summary>
+ /// <summary>密钥。一般不设置,应用默认接入</summary>
public static readonly Field Secret = FindByName("Secret");
/// <summary>类别</summary>
@@ -273,12 +304,18 @@ public partial class App
/// <summary>启用</summary>
public static readonly Field Enable = FindByName("Enable");
- /// <summary>作业数</summary>
+ /// <summary>作业数。该应用下作业个数</summary>
public static readonly Field JobCount = FindByName("JobCount");
- /// <summary>消息数</summary>
+ /// <summary>消息数。该应用下消息条数</summary>
public static readonly Field MessageCount = FindByName("MessageCount");
+ /// <summary>管理人。负责该应用的管理员</summary>
+ public static readonly Field ManagerId = FindByName("ManagerId");
+
+ /// <summary>管理者</summary>
+ public static readonly Field Manager = FindByName("Manager");
+
/// <summary>创建人</summary>
public static readonly Field CreateUserID = FindByName("CreateUserID");
@@ -315,13 +352,13 @@ public partial class App
/// <summary>编号</summary>
public const String ID = "ID";
- /// <summary>名称</summary>
+ /// <summary>名称。应用英文名</summary>
public const String Name = "Name";
- /// <summary>显示名</summary>
+ /// <summary>显示名。应用中文名</summary>
public const String DisplayName = "DisplayName";
- /// <summary>密钥</summary>
+ /// <summary>密钥。一般不设置,应用默认接入</summary>
public const String Secret = "Secret";
/// <summary>类别</summary>
@@ -336,12 +373,18 @@ public partial class App
/// <summary>启用</summary>
public const String Enable = "Enable";
- /// <summary>作业数</summary>
+ /// <summary>作业数。该应用下作业个数</summary>
public const String JobCount = "JobCount";
- /// <summary>消息数</summary>
+ /// <summary>消息数。该应用下消息条数</summary>
public const String MessageCount = "MessageCount";
+ /// <summary>管理人。负责该应用的管理员</summary>
+ public const String ManagerId = "ManagerId";
+
+ /// <summary>管理者</summary>
+ public const String Manager = "Manager";
+
/// <summary>创建人</summary>
public const String CreateUserID = "CreateUserID";
diff --git "a/AntJob.Data/Entity/\345\272\224\347\224\250\346\266\210\346\201\257.Biz.cs" "b/AntJob.Data/Entity/\345\272\224\347\224\250\346\266\210\346\201\257.Biz.cs"
index dad6748..0d8ee7f 100644
--- "a/AntJob.Data/Entity/\345\272\224\347\224\250\346\266\210\346\201\257.Biz.cs"
+++ "b/AntJob.Data/Entity/\345\272\224\347\224\250\346\266\210\346\201\257.Biz.cs"
@@ -119,7 +119,7 @@ public partial class AppMessage : EntityBase<AppMessage>
/// <returns></returns>
public static IList<AppMessage> GetTopic(Int32 appid, String topic, DateTime endTime, Int32 count)
{
- return FindAll(_.AppID == appid & _.Topic == topic & _.Id <= Meta.Factory.Snow.GetId(endTime), _.Id.Asc(), null, 0, count);
+ return FindAll(_.AppID == appid & _.Topic == topic & (_.DelayTime.IsNull() | _.DelayTime <= endTime), _.Id.Asc(), null, 0, count);
}
/// <summary>去重过滤</summary>
diff --git "a/AntJob.Data/Entity/\345\272\224\347\224\250\346\266\210\346\201\257.cs" "b/AntJob.Data/Entity/\345\272\224\347\224\250\346\266\210\346\201\257.cs"
index c796757..86c8d1f 100644
--- "a/AntJob.Data/Entity/\345\272\224\347\224\250\346\266\210\346\201\257.cs"
+++ "b/AntJob.Data/Entity/\345\272\224\347\224\250\346\266\210\346\201\257.cs"
@@ -18,6 +18,7 @@ namespace AntJob.Data.Entity;
[DataObject]
[Description("应用消息。消息调度,某些作业负责生产消息,供其它作业进行消费处理")]
[BindIndex("IX_AppMessage_AppID_Topic_UpdateTime", false, "AppID,Topic,UpdateTime")]
+[BindIndex("IX_AppMessage_AppID_Topic_DelayTime", false, "AppID,Topic,DelayTime")]
[BindTable("AppMessage", Description = "应用消息。消息调度,某些作业负责生产消息,供其它作业进行消费处理", ConnName = "Ant", DbType = DatabaseType.None)]
public partial class AppMessage
{
@@ -27,7 +28,7 @@ public partial class AppMessage
[DisplayName("编号")]
[Description("编号")]
[DataObjectField(true, false, false, 0)]
- [BindColumn("Id", "编号", "")]
+ [BindColumn("Id", "编号", "", DataScale = "time")]
public Int64 Id { get => _Id; set { if (OnPropertyChanging("Id", value)) { _Id = value; OnPropertyChanged("Id"); } } }
private Int32 _AppID;
@@ -62,6 +63,14 @@ public partial class AppMessage
[BindColumn("Data", "数据。可以是Json数据,比如StatID", "")]
public String Data { get => _Data; set { if (OnPropertyChanging("Data", value)) { _Data = value; OnPropertyChanged("Data"); } } }
+ private DateTime _DelayTime;
+ /// <summary>延迟时间。延迟到该时间执行</summary>
+ [DisplayName("延迟时间")]
+ [Description("延迟时间。延迟到该时间执行")]
+ [DataObjectField(false, false, true, 0)]
+ [BindColumn("DelayTime", "延迟时间。延迟到该时间执行", "")]
+ public DateTime DelayTime { get => _DelayTime; set { if (OnPropertyChanging("DelayTime", value)) { _DelayTime = value; OnPropertyChanged("DelayTime"); } } }
+
private String _TraceId;
/// <summary>追踪。链路追踪,用于APM性能追踪定位,还原该事件的调用链</summary>
[Category("扩展")]
@@ -121,6 +130,7 @@ public partial class AppMessage
"JobID" => _JobID,
"Topic" => _Topic,
"Data" => _Data,
+ "DelayTime" => _DelayTime,
"TraceId" => _TraceId,
"CreateIP" => _CreateIP,
"CreateTime" => _CreateTime,
@@ -137,6 +147,7 @@ public partial class AppMessage
case "JobID": _JobID = value.ToInt(); break;
case "Topic": _Topic = Convert.ToString(value); break;
case "Data": _Data = Convert.ToString(value); break;
+ case "DelayTime": _DelayTime = value.ToDateTime(); break;
case "TraceId": _TraceId = Convert.ToString(value); break;
case "CreateIP": _CreateIP = Convert.ToString(value); break;
case "CreateTime": _CreateTime = value.ToDateTime(); break;
@@ -167,6 +178,39 @@ public partial class AppMessage
#endregion
+ #region 扩展查询
+ /// <summary>根据编号查找</summary>
+ /// <param name="id">编号</param>
+ /// <returns>实体对象</returns>
+ public static AppMessage FindById(Int64 id)
+ {
+ if (id < 0) return null;
+
+ return Find(_.Id == id);
+ }
+
+ /// <summary>根据应用查找</summary>
+ /// <param name="appId">应用</param>
+ /// <returns>实体列表</returns>
+ public static IList<AppMessage> FindAllByAppID(Int32 appId)
+ {
+ if (appId < 0) return [];
+
+ return FindAll(_.AppID == appId);
+ }
+ #endregion
+
+ #region 数据清理
+ /// <summary>清理指定时间段内的数据</summary>
+ /// <param name="start">开始时间。未指定时清理小于指定时间的所有数据</param>
+ /// <param name="end">结束时间</param>
+ /// <returns>清理行数</returns>
+ public static Int32 DeleteWith(DateTime start, DateTime end)
+ {
+ return Delete(_.Id.Between(start, end, Meta.Factory.Snow));
+ }
+ #endregion
+
#region 字段名
/// <summary>取得应用消息字段信息的快捷方式</summary>
public partial class _
@@ -186,6 +230,9 @@ public partial class AppMessage
/// <summary>数据。可以是Json数据,比如StatID</summary>
public static readonly Field Data = FindByName("Data");
+ /// <summary>延迟时间。延迟到该时间执行</summary>
+ public static readonly Field DelayTime = FindByName("DelayTime");
+
/// <summary>追踪。链路追踪,用于APM性能追踪定位,还原该事件的调用链</summary>
public static readonly Field TraceId = FindByName("TraceId");
@@ -222,6 +269,9 @@ public partial class AppMessage
/// <summary>数据。可以是Json数据,比如StatID</summary>
public const String Data = "Data";
+ /// <summary>延迟时间。延迟到该时间执行</summary>
+ public const String DelayTime = "DelayTime";
+
/// <summary>追踪。链路追踪,用于APM性能追踪定位,还原该事件的调用链</summary>
public const String TraceId = "TraceId";
diff --git "a/AntJob.Data/Entity/\345\272\224\347\224\250\345\234\250\347\272\277.cs" "b/AntJob.Data/Entity/\345\272\224\347\224\250\345\234\250\347\272\277.cs"
index 65babe6..2a7d2f8 100644
--- "a/AntJob.Data/Entity/\345\272\224\347\224\250\345\234\250\347\272\277.cs"
+++ "b/AntJob.Data/Entity/\345\272\224\347\224\250\345\234\250\347\272\277.cs"
@@ -96,6 +96,14 @@ public partial class AppOnline
[BindColumn("Server", "服务端。客户端登录到哪个服务端,IP加端口", "")]
public String Server { get => _Server; set { if (OnPropertyChanging("Server", value)) { _Server = value; OnPropertyChanged("Server"); } } }
+ private Boolean _Enable;
+ /// <summary>启用。是否允许申请任务</summary>
+ [DisplayName("启用")]
+ [Description("启用。是否允许申请任务")]
+ [DataObjectField(false, false, false, 0)]
+ [BindColumn("Enable", "启用。是否允许申请任务", "")]
+ public Boolean Enable { get => _Enable; set { if (OnPropertyChanging("Enable", value)) { _Enable = value; OnPropertyChanged("Enable"); } } }
+
private Int32 _Tasks;
/// <summary>任务数</summary>
[DisplayName("任务数")]
@@ -133,7 +141,7 @@ public partial class AppOnline
[DisplayName("耗时")]
[Description("耗时。执行任务总耗时,秒")]
[DataObjectField(false, false, false, 0)]
- [BindColumn("Cost", "耗时。执行任务总耗时,秒", "")]
+ [BindColumn("Cost", "耗时。执行任务总耗时,秒", "", ItemType = "TimeSpan")]
public Int64 Cost { get => _Cost; set { if (OnPropertyChanging("Cost", value)) { _Cost = value; OnPropertyChanged("Cost"); } } }
private Int64 _Speed;
@@ -215,6 +223,7 @@ public partial class AppOnline
"Version" => _Version,
"CompileTime" => _CompileTime,
"Server" => _Server,
+ "Enable" => _Enable,
"Tasks" => _Tasks,
"Total" => _Total,
"Success" => _Success,
@@ -242,6 +251,7 @@ public partial class AppOnline
case "Version": _Version = Convert.ToString(value); break;
case "CompileTime": _CompileTime = value.ToDateTime(); break;
case "Server": _Server = Convert.ToString(value); break;
+ case "Enable": _Enable = value.ToBoolean(); break;
case "Tasks": _Tasks = value.ToInt(); break;
case "Total": _Total = value.ToLong(); break;
case "Success": _Success = value.ToLong(); break;
@@ -271,6 +281,9 @@ public partial class AppOnline
#endregion
+ #region 扩展查询
+ #endregion
+
#region 字段名
/// <summary>取得应用在线字段信息的快捷方式</summary>
public partial class _
@@ -302,6 +315,9 @@ public partial class AppOnline
/// <summary>服务端。客户端登录到哪个服务端,IP加端口</summary>
public static readonly Field Server = FindByName("Server");
+ /// <summary>启用。是否允许申请任务</summary>
+ public static readonly Field Enable = FindByName("Enable");
+
/// <summary>任务数</summary>
public static readonly Field Tasks = FindByName("Tasks");
@@ -371,6 +387,9 @@ public partial class AppOnline
/// <summary>服务端。客户端登录到哪个服务端,IP加端口</summary>
public const String Server = "Server";
+ /// <summary>启用。是否允许申请任务</summary>
+ public const String Enable = "Enable";
+
/// <summary>任务数</summary>
public const String Tasks = "Tasks";
diff --git "a/AntJob.Data/Entity/\344\275\234\344\270\232.Biz.cs" "b/AntJob.Data/Entity/\344\275\234\344\270\232.Biz.cs"
index 4f9d932..8fb9d5b 100644
--- "a/AntJob.Data/Entity/\344\275\234\344\270\232.Biz.cs"
+++ "b/AntJob.Data/Entity/\344\275\234\344\270\232.Biz.cs"
@@ -1,14 +1,10 @@
using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
using NewLife;
-using NewLife.Caching;
using NewLife.Data;
-using NewLife.Log;
-using NewLife.Serialization;
+using NewLife.Threading;
using XCode;
-using XCode.Membership;
+using XCode.DataAccessLayer;
namespace AntJob.Data.Entity;
@@ -43,13 +39,14 @@ public partial class Job : EntityBase<Job>
// throw new ArgumentNullException(nameof(Data), $"{Mode}调度模式要求设置Data模板");
// 参数默认值
- if (Step == 0) Step = 5;
- if (MaxRetain == 0) MaxRetain = 3;
+ var step = Step;
+ if (step == 0) step = Step = 5;
+ if (MaxRetain == 0) MaxRetain = 30;
if (MaxIdle == 0) MaxIdle = GetDefaultIdle();
if (isNew)
{
- if (!Dirtys[nameof(MaxRetry)]) MaxRetry = 10;
+ if (!Dirtys[nameof(MaxRetry)]) MaxRetry = 100;
if (!Dirtys[nameof(MaxTime)]) MaxTime = 600;
if (!Dirtys[nameof(ErrorDelay)]) ErrorDelay = 60;
if (!Dirtys[nameof(MaxIdle)]) MaxIdle = GetDefaultIdle();
@@ -59,6 +56,19 @@ public partial class Job : EntityBase<Job>
//var len = _.Remark.Length;
//if (!Remark.IsNullOrEmpty() && len > 0 && Remark.Length > len) Remark = Remark.Substring(0, len);
+ // 定时任务自动生成Cron
+ if (Mode == JobModes.Time && Cron.IsNullOrEmpty())
+ {
+ if (step < 60)
+ Cron = $"0 */{step} * * * ?";
+ else if (step % 86400 == 0 && step / 86400 < 30)
+ Cron = $"0 0 0 0/{step / 86400} * ?";
+ else if (step % 3600 == 0 && step / 3600 < 24)
+ Cron = $"0 0 0/{step / 3600} * * ?";
+ else if (step % 60 == 0 && step / 60 < 60)
+ Cron = $"0 0/{step / 60} * * * ?";
+ }
+
var app = App;
if (isNew && app != null)
{
@@ -70,7 +80,7 @@ public partial class Job : EntityBase<Job>
private Int32 GetDefaultIdle()
{
// 定时调度,取步进加一分钟
- if (Mode == JobModes.Alarm) return Step + 600;
+ if (Mode == JobModes.Time) return Step + 600;
return 3600;
}
@@ -204,8 +214,8 @@ public partial class Job : EntityBase<Job>
switch (Mode)
{
case JobModes.Data:
- case JobModes.Alarm:
- return Start.Year > 2000 && Step > 0;
+ case JobModes.Time:
+ return DataTime.Year > 2000 && Step > 0;
case JobModes.Message:
return Topic.IsNullOrEmpty();
default:
@@ -215,6 +225,44 @@ public partial class Job : EntityBase<Job>
return false;
}
+ /// <summary>获取下一次执行时间</summary>
+ /// <returns></returns>
+ public DateTime GetNext()
+ {
+ var step = Step;
+ if (step <= 0) step = 30;
+
+ switch (Mode)
+ {
+ case JobModes.Data:
+ break;
+ case JobModes.Time:
+ if (!Cron.IsNullOrEmpty())
+ {
+ var time = DataTime.Year > 2000 ? DataTime : DateTime.Now;
+ var next = DateTime.MaxValue;
+ foreach (var item in Cron.Split(";"))
+ {
+ var cron = new Cron(item);
+ var dt = cron.GetNext(time);
+ if (dt < next) next = dt;
+ }
+ return next;
+ //return NewLife.Threading.Cron.GetNext(Cron.Split(";"), time);
+ }
+ else
+ {
+ return DataTime.AddSeconds(step);
+ }
+ case JobModes.Message:
+ break;
+ default:
+ break;
+ }
+
+ return DateTime.MinValue;
+ }
+
/// <summary>重置任务,让它从新开始工作</summary>
/// <param name="days">重置到多少天之前</param>
/// <param name="stime">开始时间(优先级低于days)</param>
@@ -223,14 +271,14 @@ public partial class Job : EntityBase<Job>
{
if (days < 0)
{
- Start = DateTime.MinValue;
+ //DataTime = DateTime.MinValue;
if (stime > DateTime.MinValue)
- Start = stime;
+ DataTime = stime;
End = etime;
}
else
- Start = DateTime.Now.Date.AddDays(-days);
+ DataTime = DateTime.Now.Date.AddDays(-days);
Save();
}
@@ -269,7 +317,7 @@ public partial class Job : EntityBase<Job>
{
// 如果禁用,仅返回最简单的字段
// 缺少开始时间赋值,会导致客户端启动校验失败,Job没有启用的状态下服务器报错无法正常启动
- if (!Enable) return new JobModel { Name = Name, Enable = Enable, Start = Start };
+ if (!Enable) return new JobModel { Name = Name, Enable = Enable, DataTime = DataTime };
return new JobModel
{
@@ -277,8 +325,9 @@ public partial class Job : EntityBase<Job>
ClassName = ClassName,
Enable = Enable,
- Start = Start,
+ DataTime = DataTime,
End = End,
+ Cron = Cron,
Topic = Topic,
Data = Data,
@@ -291,266 +340,4 @@ public partial class Job : EntityBase<Job>
};
}
#endregion
-
- #region 申请任务
- /// <summary>用于表示切片批次的序号</summary>
- private static Int32 _idxBatch;
-
- /// <summary>申请任务分片</summary>
- /// <param name="server">申请任务的服务端</param>
- /// <param name="ip">申请任务的IP</param>
- /// <param name="pid">申请任务的服务端进程ID</param>
- /// <param name="count">要申请的任务个数</param>
- /// <param name="cache">缓存对象</param>
- /// <returns></returns>
- public IList<JobTask> Acquire(String server, String ip, Int32 pid, Int32 count, ICache cache)
- {
- var list = new List<JobTask>();
-
- if (!Enable) return list;
-
- var step = Step;
- if (step <= 0) step = 30;
-
- //// 全局锁,确保单个作业只有一个线程在分配作业
- //using var ck = cache.AcquireLock($"Job:{ID}", 5_000);
-
- using var ts = Meta.CreateTrans();
- var start = Start;
- for (var i = 0; i < count; i++)
- {
- if (!TrySplit(start, step, out var end)) break;
-
- // 创建新的分片
- var ti = new JobTask
- {
- AppID = AppID,
- JobID = ID,
- Start = start,
- End = end,
- BatchSize = BatchSize,
- };
-
- ti.Server = server;
- ti.ProcessID = Interlocked.Increment(ref _idxBatch);
- ti.Client = $"{ip}@{pid}";
- ti.Status = JobStatus.就绪;
- ti.CreateTime = DateTime.Now;
- ti.UpdateTime = DateTime.Now;
-
- //// 如果有模板,则进行计算替换
- //if (!Data.IsNullOrEmpty()) ti.Data = TemplateHelper.Build(Data, ti.Start, ti.End);
-
- // 重复切片判断
- var key = $"job:task:{ID}:{start:yyyyMMddHHmmss}";
- if (!cache.Add(key, ti, 30))
- {
- var ti2 = cache.Get<JobTask>(key);
- XTrace.WriteLine("[{0}]重复切片:{1}", key, ti2?.ToJson());
- using var span = DefaultTracer.Instance?.NewSpan($"job:AcquireDuplicate", ti2);
- }
- else
- {
- ti.Insert();
-
- list.Add(ti);
- }
-
- // 更新任务
- Start = end;
- start = end;
- }
-
- if (list.Count > 0)
- {
- // 任务需要ID,不能批量插入优化
- //list.Insert(null);
-
- UpdateTime = DateTime.Now;
- Save();
- ts.Commit();
- }
-
- return list;
- }
-
- /// <summary>尝试分割时间片</summary>
- /// <param name="start"></param>
- /// <param name="step"></param>
- /// <param name="end"></param>
- /// <returns></returns>
- public Boolean TrySplit(DateTime start, Int32 step, out DateTime end)
- {
- // 当前时间减去偏移量,作为当前时间。数据抽取不许超过该时间
- var now = DateTime.Now.AddSeconds(-Offset);
- // 去掉毫秒
- now = now.Trim();
-
- end = DateTime.MinValue;
-
- // 开始时间和结束时间是否越界
- if (start >= now) return false;
-
- if (step <= 0) step = 30;
-
- // 必须严格要求按照步进大小分片,除非有合适的End
- end = start.AddSeconds(step);
- // 任务结束时间超过作业结束时间时,取后者
- if (End.Year > 2000 && end > End) end = End;
-
- // 时间片必须严格要求按照步进大小分片,除非有合适的End
- if (Mode != JobModes.Alarm)
- {
- if (end > now) return false;
- }
-
- // 时间区间判断
- if (start >= end) return false;
-
- return true;
- }
-
- /// <summary>申请历史错误或中断的任务</summary>
- /// <param name="server">申请任务的服务端</param>
- /// <param name="ip">申请任务的IP</param>
- /// <param name="pid">申请任务的服务端进程ID</param>
- /// <param name="count">要申请的任务个数</param>
- /// <param name="cache">缓存对象</param>
- /// <returns></returns>
- public IList<JobTask> AcquireOld(String server, String ip, Int32 pid, Int32 count, ICache cache)
- {
- //// 全局锁,确保单个作业只有一个线程在分配作业
- //using var ck = cache.AcquireLock($"Job:{ID}", 5_000);
-
- using var ts = Meta.CreateTrans();
- var list = new List<JobTask>();
-
- // 查找历史错误任务
- if (ErrorDelay > 0)
- {
- var dt = DateTime.Now.AddSeconds(-ErrorDelay);
- var list2 = JobTask.Search(ID, dt, MaxRetry, new[] { JobStatus.错误, JobStatus.取消 }, count);
- if (list2.Count > 0) list.AddRange(list2);
- }
-
- // 查找历史中断任务,持续10分钟仍然未完成
- if (MaxTime > 0 && list.Count < count)
- {
- var dt = DateTime.Now.AddSeconds(-MaxTime);
- var list2 = JobTask.Search(ID, dt, MaxRetry, new[] { JobStatus.就绪, JobStatus.抽取中, JobStatus.处理中 }, count - list.Count);
- if (list2.Count > 0) list.AddRange(list2);
- }
- if (list.Count > 0)
- {
- foreach (var ti in list)
- {
- ti.Server = server;
- ti.ProcessID = Interlocked.Increment(ref _idxBatch);
- ti.Client = $"{ip}@{pid}";
- //ti.Status = JobStatus.就绪;
- ti.CreateTime = DateTime.Now;
- ti.UpdateTime = DateTime.Now;
- }
- list.Save();
- }
-
- ts.Commit();
-
- return list;
- }
-
- /// <summary>申请任务分片</summary>
- /// <param name="topic">主题</param>
- /// <param name="server">申请任务的服务端</param>
- /// <param name="ip">申请任务的IP</param>
- /// <param name="pid">申请任务的服务端进程ID</param>
- /// <param name="count">要申请的任务个数</param>
- /// <param name="cache">缓存对象</param>
- /// <returns></returns>
- public IList<JobTask> AcquireMessage(String topic, String server, String ip, Int32 pid, Int32 count, ICache cache)
- {
- // 消费消息时,保存主题
- if (Topic != topic)
- {
- Topic = topic;
- SaveAsync();
- }
-
- var list = new List<JobTask>();
-
- if (!Enable) return list;
-
- // 验证消息数
- var now = DateTime.Now;
- if (MessageCount == 0 && UpdateTime.AddMinutes(2) > now) return list;
-
- //// 全局锁,确保单个作业只有一个线程在分配作业
- //using var ck = cache.AcquireLock($"Job:{ID}", 5_000);
-
- using var ts = Meta.CreateTrans();
- var size = BatchSize;
- if (size == 0) size = 1;
-
- // 消费消息。请求任务数量=空闲线程*批大小
- var msgs = AppMessage.GetTopic(AppID, topic, now, count * size);
- if (msgs.Count > 0)
- {
- for (var i = 0; i < msgs.Count;)
- {
- var msgList = msgs.Skip(i).Take(size).ToList();
- if (msgList.Count == 0) break;
-
- i += msgList.Count;
-
- // 创建新的分片
- var ti = new JobTask
- {
- AppID = AppID,
- JobID = ID,
- Data = msgList.Select(e => e.Data).ToJson(),
- MsgCount = msgList.Count,
-
- BatchSize = size,
- };
-
- ti.Server = server;
- ti.ProcessID = Interlocked.Increment(ref _idxBatch);
- ti.Client = $"{ip}@{pid}";
- ti.Status = JobStatus.就绪;
- ti.CreateTime = DateTime.Now;
- ti.UpdateTime = DateTime.Now;
-
- ti.Insert();
-
- // 从去重缓存去掉
- cache.Remove(msgList.Select(e => $"antjob:{AppID}:{Topic}:{e}").ToArray());
-
- list.Add(ti);
- }
-
- // 批量删除消息
- msgs.Delete();
- }
-
- // 更新作业下的消息数
- MessageCount = AppMessage.FindCountByAppIDAndTopic(AppID, topic);
- UpdateTime = now;
- Save();
-
- // 消费完成后,更新应用的消息数
- if (MessageCount == 0)
- {
- var app = App;
- if (app != null)
- {
- app.MessageCount = AppMessage.FindCountByAppID(ID);
- app.SaveAsync();
- }
- }
-
- ts.Commit();
-
- return list;
- }
- #endregion
}
\ No newline at end of file
diff --git "a/AntJob.Data/Entity/\344\275\234\344\270\232.cs" "b/AntJob.Data/Entity/\344\275\234\344\270\232.cs"
index dfb5b71..4c90f5d 100644
--- "a/AntJob.Data/Entity/\344\275\234\344\270\232.cs"
+++ "b/AntJob.Data/Entity/\344\275\234\344\270\232.cs"
@@ -13,12 +13,12 @@ using XCode.DataAccessLayer;
namespace AntJob.Data.Entity;
-/// <summary>作业。数据计算逻辑的主要单元,每个作业即是一个业务逻辑,各个作业之前存在依赖关系</summary>
+/// <summary>作业。数据计算逻辑的主要单元,每个作业即是一个业务逻辑</summary>
[Serializable]
[DataObject]
-[Description("作业。数据计算逻辑的主要单元,每个作业即是一个业务逻辑,各个作业之前存在依赖关系")]
+[Description("作业。数据计算逻辑的主要单元,每个作业即是一个业务逻辑")]
[BindIndex("IU_Job_AppID_Name", true, "AppID,Name")]
-[BindTable("Job", Description = "作业。数据计算逻辑的主要单元,每个作业即是一个业务逻辑,各个作业之前存在依赖关系", ConnName = "Ant", DbType = DatabaseType.None)]
+[BindTable("Job", Description = "作业。数据计算逻辑的主要单元,每个作业即是一个业务逻辑", ConnName = "Ant", DbType = DatabaseType.None)]
public partial class Job
{
#region 属性
@@ -62,6 +62,14 @@ public partial class Job
[BindColumn("DisplayName", "显示名", "")]
public String DisplayName { get => _DisplayName; set { if (OnPropertyChanging("DisplayName", value)) { _DisplayName = value; OnPropertyChanged("DisplayName"); } } }
+ private Boolean _Enable;
+ /// <summary>启用</summary>
+ [DisplayName("启用")]
+ [Description("启用")]
+ [DataObjectField(false, false, false, 0)]
+ [BindColumn("Enable", "启用", "")]
+ public Boolean Enable { get => _Enable; set { if (OnPropertyChanging("Enable", value)) { _Enable = value; OnPropertyChanged("Enable"); } } }
+
private JobModes _Mode;
/// <summary>调度模式。定时调度只要达到时间片开头就可以跑,数据调度要求达到时间片末尾才可以跑</summary>
[DisplayName("调度模式")]
@@ -70,6 +78,14 @@ public partial class Job
[BindColumn("Mode", "调度模式。定时调度只要达到时间片开头就可以跑,数据调度要求达到时间片末尾才可以跑", "")]
public JobModes Mode { get => _Mode; set { if (OnPropertyChanging("Mode", value)) { _Mode = value; OnPropertyChanged("Mode"); } } }
+ private String _Cron;
+ /// <summary>执行频次。定时调度的Cron表达式</summary>
+ [DisplayName("执行频次")]
+ [Description("执行频次。定时调度的Cron表达式")]
+ [DataObjectField(false, false, true, 50)]
+ [BindColumn("Cron", "执行频次。定时调度的Cron表达式", "")]
+ public String Cron { get => _Cron; set { if (OnPropertyChanging("Cron", value)) { _Cron = value; OnPropertyChanged("Cron"); } } }
+
private String _Topic;
/// <summary>主题。消息调度时消费的主题</summary>
[DisplayName("主题")]
@@ -86,20 +102,20 @@ public partial class Job
[BindColumn("MessageCount", "消息数", "")]
public Int32 MessageCount { get => _MessageCount; set { if (OnPropertyChanging("MessageCount", value)) { _MessageCount = value; OnPropertyChanged("MessageCount"); } } }
- private DateTime _Start;
- /// <summary>开始。大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用</summary>
- [DisplayName("开始")]
- [Description("开始。大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用")]
+ private DateTime _DataTime;
+ /// <summary>数据时间。下一次处理数据的时间,默认从当前时间开始</summary>
+ [DisplayName("数据时间")]
+ [Description("数据时间。下一次处理数据的时间,默认从当前时间开始")]
[DataObjectField(false, false, true, 0)]
- [BindColumn("Start", "开始。大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用", "")]
- public DateTime Start { get => _Start; set { if (OnPropertyChanging("Start", value)) { _Start = value; OnPropertyChanged("Start"); } } }
+ [BindColumn("DataTime", "数据时间。下一次处理数据的时间,默认从当前时间开始", "")]
+ public DateTime DataTime { get => _DataTime; set { if (OnPropertyChanging("DataTime", value)) { _DataTime = value; OnPropertyChanged("DataTime"); } } }
private DateTime _End;
- /// <summary>结束。小于不等于,数据调度到达该时间点后触发(可能有偏移量),默认空表示无止境,消息调度不适用</summary>
+ /// <summary>结束。到该时间停止调度作业,默认不设置时永不停止</summary>
[DisplayName("结束")]
- [Description("结束。小于不等于,数据调度到达该时间点后触发(可能有偏移量),默认空表示无止境,消息调度不适用")]
+ [Description("结束。到该时间停止调度作业,默认不设置时永不停止")]
[DataObjectField(false, false, true, 0)]
- [BindColumn("End", "结束。小于不等于,数据调度到达该时间点后触发(可能有偏移量),默认空表示无止境,消息调度不适用", "")]
+ [BindColumn("End", "结束。到该时间停止调度作业,默认不设置时永不停止", "")]
public DateTime End { get => _End; set { if (OnPropertyChanging("End", value)) { _End = value; OnPropertyChanged("End"); } } }
private Int32 _Step;
@@ -107,7 +123,7 @@ public partial class Job
[DisplayName("步进")]
[Description("步进。切分任务的时间区间,秒")]
[DataObjectField(false, false, false, 0)]
- [BindColumn("Step", "步进。切分任务的时间区间,秒", "")]
+ [BindColumn("Step", "步进。切分任务的时间区间,秒", "", ItemType = "TimeSpan")]
public Int32 Step { get => _Step; set { if (OnPropertyChanging("Step", value)) { _Step = value; OnPropertyChanged("Step"); } } }
private Int32 _BatchSize;
@@ -123,11 +139,12 @@ public partial class Job
[DisplayName("偏移")]
[Description("偏移。距离AntServer当前时间的秒数,避免因服务器之间的时间误差而错过部分数据,秒")]
[DataObjectField(false, false, false, 0)]
- [BindColumn("Offset", "偏移。距离AntServer当前时间的秒数,避免因服务器之间的时间误差而错过部分数据,秒", "")]
+ [BindColumn("Offset", "偏移。距离AntServer当前时间的秒数,避免因服务器之间的时间误差而错过部分数据,秒", "", ItemType = "TimeSpan")]
public Int32 Offset { get => _Offset; set { if (OnPropertyChanging("Offset", value)) { _Offset = value; OnPropertyChanged("Offset"); } } }
private Int32 _MaxTask;
/// <summary>并行度。一共允许多少个任务并行处理,多执行端时平均分配,确保该作业整体并行度</summary>
+ [Category("控制参数")]
[DisplayName("并行度")]
[Description("并行度。一共允许多少个任务并行处理,多执行端时平均分配,确保该作业整体并行度")]
[DataObjectField(false, false, false, 0)]
@@ -136,6 +153,7 @@ public partial class Job
private Int32 _MaxError;
/// <summary>最大错误。连续错误达到最大错误数时停止</summary>
+ [Category("控制参数")]
[DisplayName("最大错误")]
[Description("最大错误。连续错误达到最大错误数时停止")]
[DataObjectField(false, false, false, 0)]
@@ -144,6 +162,7 @@ public partial class Job
private Int32 _MaxRetry;
/// <summary>最大重试。默认10次,超过该次数后将不再重试</summary>
+ [Category("控制参数")]
[DisplayName("最大重试")]
[Description("最大重试。默认10次,超过该次数后将不再重试")]
[DataObjectField(false, false, false, 0)]
@@ -152,38 +171,52 @@ public partial class Job
private Int32 _MaxTime;
/// <summary>最大执行时间。默认600秒,超过该时间则认为执行器故障,将会把该任务分配给其它执行器</summary>
+ [Category("控制参数")]
[DisplayName("最大执行时间")]
[Description("最大执行时间。默认600秒,超过该时间则认为执行器故障,将会把该任务分配给其它执行器")]
[DataObjectField(false, false, false, 0)]
- [BindColumn("MaxTime", "最大执行时间。默认600秒,超过该时间则认为执行器故障,将会把该任务分配给其它执行器", "")]
+ [BindColumn("MaxTime", "最大执行时间。默认600秒,超过该时间则认为执行器故障,将会把该任务分配给其它执行器", "", ItemType = "TimeSpan")]
public Int32 MaxTime { get => _MaxTime; set { if (OnPropertyChanging("MaxTime", value)) { _MaxTime = value; OnPropertyChanged("MaxTime"); } } }
private Int32 _MaxRetain;
- /// <summary>保留。任务项保留天数,超过天数的任务项将被删除,默认3天</summary>
+ /// <summary>保留。任务项保留天数,超过天数的任务项将被删除,默认30天</summary>
+ [Category("控制参数")]
[DisplayName("保留")]
- [Description("保留。任务项保留天数,超过天数的任务项将被删除,默认3天")]
+ [Description("保留。任务项保留天数,超过天数的任务项将被删除,默认30天")]
[DataObjectField(false, false, false, 0)]
- [BindColumn("MaxRetain", "保留。任务项保留天数,超过天数的任务项将被删除,默认3天", "")]
+ [BindColumn("MaxRetain", "保留。任务项保留天数,超过天数的任务项将被删除,默认30天", "")]
public Int32 MaxRetain { get => _MaxRetain; set { if (OnPropertyChanging("MaxRetain", value)) { _MaxRetain = value; OnPropertyChanged("MaxRetain"); } } }
private Int32 _MaxIdle;
/// <summary>最大空闲时间。默认3600秒,超过该时间不更新则认为应用程序故障,系统触发告警</summary>
+ [Category("控制参数")]
[DisplayName("最大空闲时间")]
[Description("最大空闲时间。默认3600秒,超过该时间不更新则认为应用程序故障,系统触发告警")]
[DataObjectField(false, false, false, 0)]
- [BindColumn("MaxIdle", "最大空闲时间。默认3600秒,超过该时间不更新则认为应用程序故障,系统触发告警", "")]
+ [BindColumn("MaxIdle", "最大空闲时间。默认3600秒,超过该时间不更新则认为应用程序故障,系统触发告警", "", ItemType = "TimeSpan")]
public Int32 MaxIdle { get => _MaxIdle; set { if (OnPropertyChanging("MaxIdle", value)) { _MaxIdle = value; OnPropertyChanged("MaxIdle"); } } }
private Int32 _ErrorDelay;
/// <summary>错误延迟。默认60秒,出错延迟后重新发放</summary>
+ [Category("控制参数")]
[DisplayName("错误延迟")]
[Description("错误延迟。默认60秒,出错延迟后重新发放")]
[DataObjectField(false, false, false, 0)]
- [BindColumn("ErrorDelay", "错误延迟。默认60秒,出错延迟后重新发放", "")]
+ [BindColumn("ErrorDelay", "错误延迟。默认60秒,出错延迟后重新发放", "", ItemType = "TimeSpan")]
public Int32 ErrorDelay { get => _ErrorDelay; set { if (OnPropertyChanging("ErrorDelay", value)) { _ErrorDelay = value; OnPropertyChanged("ErrorDelay"); } } }
+ private DateTime _Deadline;
+ /// <summary>最后期限。超过该时间后,任务将不再执行</summary>
+ [Category("控制参数")]
+ [DisplayName("最后期限")]
+ [Description("最后期限。超过该时间后,任务将不再执行")]
+ [DataObjectField(false, false, true, 0)]
+ [BindColumn("Deadline", "最后期限。超过该时间后,任务将不再执行", "")]
+ public DateTime Deadline { get => _Deadline; set { if (OnPropertyChanging("Deadline", value)) { _Deadline = value; OnPropertyChanged("Deadline"); } } }
+
private Int64 _Total;
/// <summary>总数。任务处理的总数据,例如数据调度抽取得到的总行数,定时调度默认1</summary>
+ [Category("统计")]
[DisplayName("总数")]
[Description("总数。任务处理的总数据,例如数据调度抽取得到的总行数,定时调度默认1")]
[DataObjectField(false, false, false, 0)]
@@ -192,6 +225,7 @@ public partial class Job
private Int64 _Success;
/// <summary>成功。成功处理的数据,取自于Handler.Execute返回值,或者ProcessItem返回true的个数</summary>
+ [Category("统计")]
[DisplayName("成功")]
[Description("成功。成功处理的数据,取自于Handler.Execute返回值,或者ProcessItem返回true的个数")]
[DataObjectField(false, false, false, 0)]
@@ -200,6 +234,7 @@ public partial class Job
private Int32 _Error;
/// <summary>错误</summary>
+ [Category("统计")]
[DisplayName("错误")]
[Description("错误")]
[DataObjectField(false, false, false, 0)]
@@ -208,6 +243,7 @@ public partial class Job
private Int32 _Times;
/// <summary>次数</summary>
+ [Category("统计")]
[DisplayName("次数")]
[Description("次数")]
[DataObjectField(false, false, false, 0)]
@@ -216,19 +252,30 @@ public partial class Job
private Int32 _Speed;
/// <summary>速度</summary>
+ [Category("统计")]
[DisplayName("速度")]
[Description("速度")]
[DataObjectField(false, false, false, 0)]
[BindColumn("Speed", "速度", "")]
public Int32 Speed { get => _Speed; set { if (OnPropertyChanging("Speed", value)) { _Speed = value; OnPropertyChanged("Speed"); } } }
- private Boolean _Enable;
- /// <summary>启用</summary>
- [DisplayName("启用")]
- [Description("启用")]
+ private JobStatus _LastStatus;
+ /// <summary>最后状态。最后一次状态</summary>
+ [Category("统计")]
+ [DisplayName("最后状态")]
+ [Description("最后状态。最后一次状态")]
[DataObjectField(false, false, false, 0)]
- [BindColumn("Enable", "启用", "")]
- public Boolean Enable { get => _Enable; set { if (OnPropertyChanging("Enable", value)) { _Enable = value; OnPropertyChanged("Enable"); } } }
+ [BindColumn("LastStatus", "最后状态。最后一次状态", "")]
+ public JobStatus LastStatus { get => _LastStatus; set { if (OnPropertyChanging("LastStatus", value)) { _LastStatus = value; OnPropertyChanged("LastStatus"); } } }
+
+ private DateTime _LastTime;
+ /// <summary>最后时间。最后一次时间</summary>
+ [Category("统计")]
+ [DisplayName("最后时间")]
+ [Description("最后时间。最后一次时间")]
+ [DataObjectField(false, false, true, 0)]
+ [BindColumn("LastTime", "最后时间。最后一次时间", "")]
+ public DateTime LastTime { get => _LastTime; set { if (OnPropertyChanging("LastTime", value)) { _LastTime = value; OnPropertyChanged("LastTime"); } } }
private String _Data;
/// <summary>数据。Sql模板或C#模板</summary>
@@ -333,10 +380,12 @@ public partial class Job
"Name" => _Name,
"ClassName" => _ClassName,
"DisplayName" => _DisplayName,
+ "Enable" => _Enable,
"Mode" => _Mode,
+ "Cron" => _Cron,
"Topic" => _Topic,
"MessageCount" => _MessageCount,
- "Start" => _Start,
+ "DataTime" => _DataTime,
"End" => _End,
"Step" => _Step,
"BatchSize" => _BatchSize,
@@ -348,12 +397,14 @@ public partial class Job
"MaxRetain" => _MaxRetain,
"MaxIdle" => _MaxIdle,
"ErrorDelay" => _ErrorDelay,
+ "Deadline" => _Deadline,
"Total" => _Total,
"Success" => _Success,
"Error" => _Error,
"Times" => _Times,
"Speed" => _Speed,
- "Enable" => _Enable,
+ "LastStatus" => _LastStatus,
+ "LastTime" => _LastTime,
"Data" => _Data,
"CreateUserID" => _CreateUserID,
"CreateUser" => _CreateUser,
@@ -375,10 +426,12 @@ public partial class Job
case "Name": _Name = Convert.ToString(value); break;
case "ClassName": _ClassName = Convert.ToString(value); break;
case "DisplayName": _DisplayName = Convert.ToString(value); break;
+ case "Enable": _Enable = value.ToBoolean(); break;
case "Mode": _Mode = (JobModes)value.ToInt(); break;
+ case "Cron": _Cron = Convert.ToString(value); break;
case "Topic": _Topic = Convert.ToString(value); break;
case "MessageCount": _MessageCount = value.ToInt(); break;
- case "Start": _Start = value.ToDateTime(); break;
+ case "DataTime": _DataTime = value.ToDateTime(); break;
case "End": _End = value.ToDateTime(); break;
case "Step": _Step = value.ToInt(); break;
case "BatchSize": _BatchSize = value.ToInt(); break;
@@ -390,12 +443,14 @@ public partial class Job
case "MaxRetain": _MaxRetain = value.ToInt(); break;
case "MaxIdle": _MaxIdle = value.ToInt(); break;
case "ErrorDelay": _ErrorDelay = value.ToInt(); break;
+ case "Deadline": _Deadline = value.ToDateTime(); break;
case "Total": _Total = value.ToLong(); break;
case "Success": _Success = value.ToLong(); break;
case "Error": _Error = value.ToInt(); break;
case "Times": _Times = value.ToInt(); break;
case "Speed": _Speed = value.ToInt(); break;
- case "Enable": _Enable = value.ToBoolean(); break;
+ case "LastStatus": _LastStatus = (JobStatus)value.ToInt(); break;
+ case "LastTime": _LastTime = value.ToDateTime(); break;
case "Data": _Data = Convert.ToString(value); break;
case "CreateUserID": _CreateUserID = value.ToInt(); break;
case "CreateUser": _CreateUser = Convert.ToString(value); break;
@@ -423,6 +478,9 @@ public partial class Job
#endregion
+ #region 扩展查询
+ #endregion
+
#region 字段名
/// <summary>取得作业字段信息的快捷方式</summary>
public partial class _
@@ -442,19 +500,25 @@ public partial class Job
/// <summary>显示名</summary>
public static readonly Field DisplayName = FindByName("DisplayName");
+ /// <summary>启用</summary>
+ public static readonly Field Enable = FindByName("Enable");
+
/// <summary>调度模式。定时调度只要达到时间片开头就可以跑,数据调度要求达到时间片末尾才可以跑</summary>
public static readonly Field Mode = FindByName("Mode");
+ /// <summary>执行频次。定时调度的Cron表达式</summary>
+ public static readonly Field Cron = FindByName("Cron");
+
/// <summary>主题。消息调度时消费的主题</summary>
public static readonly Field Topic = FindByName("Topic");
/// <summary>消息数</summary>
public static readonly Field MessageCount = FindByName("MessageCount");
- /// <summary>开始。大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用</summary>
- public static readonly Field Start = FindByName("Start");
+ /// <summary>数据时间。下一次处理数据的时间,默认从当前时间开始</summary>
+ public static readonly Field DataTime = FindByName("DataTime");
- /// <summary>结束。小于不等于,数据调度到达该时间点后触发(可能有偏移量),默认空表示无止境,消息调度不适用</summary>
+ /// <summary>结束。到该时间停止调度作业,默认不设置时永不停止</summary>
public static readonly Field End = FindByName("End");
/// <summary>步进。切分任务的时间区间,秒</summary>
@@ -478,7 +542,7 @@ public partial class Job
/// <summary>最大执行时间。默认600秒,超过该时间则认为执行器故障,将会把该任务分配给其它执行器</summary>
public static readonly Field MaxTime = FindByName("MaxTime");
- /// <summary>保留。任务项保留天数,超过天数的任务项将被删除,默认3天</summary>
+ /// <summary>保留。任务项保留天数,超过天数的任务项将被删除,默认30天</summary>
public static readonly Field MaxRetain = FindByName("MaxRetain");
/// <summary>最大空闲时间。默认3600秒,超过该时间不更新则认为应用程序故障,系统触发告警</summary>
@@ -487,6 +551,9 @@ public partial class Job
/// <summary>错误延迟。默认60秒,出错延迟后重新发放</summary>
public static readonly Field ErrorDelay = FindByName("ErrorDelay");
+ /// <summary>最后期限。超过该时间后,任务将不再执行</summary>
+ public static readonly Field Deadline = FindByName("Deadline");
+
/// <summary>总数。任务处理的总数据,例如数据调度抽取得到的总行数,定时调度默认1</summary>
public static readonly Field Total = FindByName("Total");
@@ -502,8 +569,11 @@ public partial class Job
/// <summary>速度</summary>
public static readonly Field Speed = FindByName("Speed");
- /// <summary>启用</summary>
- public static readonly Field Enable = FindByName("Enable");
+ /// <summary>最后状态。最后一次状态</summary>
+ public static readonly Field LastStatus = FindByName("LastStatus");
+
+ /// <summary>最后时间。最后一次时间</summary>
+ public static readonly Field LastTime = FindByName("LastTime");
/// <summary>数据。Sql模板或C#模板</summary>
public static readonly Field Data = FindByName("Data");
@@ -556,19 +626,25 @@ public partial class Job
/// <summary>显示名</summary>
public const String DisplayName = "DisplayName";
+ /// <summary>启用</summary>
+ public const String Enable = "Enable";
+
/// <summary>调度模式。定时调度只要达到时间片开头就可以跑,数据调度要求达到时间片末尾才可以跑</summary>
public const String Mode = "Mode";
+ /// <summary>执行频次。定时调度的Cron表达式</summary>
+ public const String Cron = "Cron";
+
/// <summary>主题。消息调度时消费的主题</summary>
public const String Topic = "Topic";
/// <summary>消息数</summary>
public const String MessageCount = "MessageCount";
- /// <summary>开始。大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用</summary>
- public const String Start = "Start";
+ /// <summary>数据时间。下一次处理数据的时间,默认从当前时间开始</summary>
+ public const String DataTime = "DataTime";
- /// <summary>结束。小于不等于,数据调度到达该时间点后触发(可能有偏移量),默认空表示无止境,消息调度不适用</summary>
+ /// <summary>结束。到该时间停止调度作业,默认不设置时永不停止</summary>
public const String End = "End";
/// <summary>步进。切分任务的时间区间,秒</summary>
@@ -592,7 +668,7 @@ public partial class Job
/// <summary>最大执行时间。默认600秒,超过该时间则认为执行器故障,将会把该任务分配给其它执行器</summary>
public const String MaxTime = "MaxTime";
- /// <summary>保留。任务项保留天数,超过天数的任务项将被删除,默认3天</summary>
+ /// <summary>保留。任务项保留天数,超过天数的任务项将被删除,默认30天</summary>
public const String MaxRetain = "MaxRetain";
/// <summary>最大空闲时间。默认3600秒,超过该时间不更新则认为应用程序故障,系统触发告警</summary>
@@ -601,6 +677,9 @@ public partial class Job
/// <summary>错误延迟。默认60秒,出错延迟后重新发放</summary>
public const String ErrorDelay = "ErrorDelay";
+ /// <summary>最后期限。超过该时间后,任务将不再执行</summary>
+ public const String Deadline = "Deadline";
+
/// <summary>总数。任务处理的总数据,例如数据调度抽取得到的总行数,定时调度默认1</summary>
public const String Total = "Total";
@@ -616,8 +695,11 @@ public partial class Job
/// <summary>速度</summary>
public const String Speed = "Speed";
- /// <summary>启用</summary>
- public const String Enable = "Enable";
+ /// <summary>最后状态。最后一次状态</summary>
+ public const String LastStatus = "LastStatus";
+
+ /// <summary>最后时间。最后一次时间</summary>
+ public const String LastTime = "LastTime";
/// <summary>数据。Sql模板或C#模板</summary>
public const String Data = "Data";
diff --git "a/AntJob.Data/Entity/\344\275\234\344\270\232\351\224\231\350\257\257.Biz.cs" "b/AntJob.Data/Entity/\344\275\234\344\270\232\351\224\231\350\257\257.Biz.cs"
index a222e5e..7f3eca5 100644
--- "a/AntJob.Data/Entity/\344\275\234\344\270\232\351\224\231\350\257\257.Biz.cs"
+++ "b/AntJob.Data/Entity/\344\275\234\344\270\232\351\224\231\350\257\257.Biz.cs"
@@ -116,7 +116,7 @@ public partial class JobError : EntityBase<JobError>
if (jobid > 0) exp &= _.JobID == jobid;
if (!client.IsNullOrEmpty()) exp &= _.Client == client;
if (!key.IsNullOrEmpty()) exp &= _.Message.Contains(key);
- exp &= _.Start.Between(start, end);
+ exp &= _.DataTime.Between(start, end);
return FindAll(exp, p);
}
diff --git "a/AntJob.Data/Entity/\344\275\234\344\270\232\351\224\231\350\257\257.cs" "b/AntJob.Data/Entity/\344\275\234\344\270\232\351\224\231\350\257\257.cs"
index 90160a1..31396f0 100644
--- "a/AntJob.Data/Entity/\344\275\234\344\270\232\351\224\231\350\257\257.cs"
+++ "b/AntJob.Data/Entity/\344\275\234\344\270\232\351\224\231\350\257\257.cs"
@@ -63,13 +63,13 @@ public partial class JobError
[BindColumn("Client", "客户端。IP加进程", "")]
public String Client { get => _Client; set { if (OnPropertyChanging("Client", value)) { _Client = value; OnPropertyChanged("Client"); } } }
- private DateTime _Start;
- /// <summary>开始。大于等于</summary>
- [DisplayName("开始")]
- [Description("开始。大于等于")]
+ private DateTime _DataTime;
+ /// <summary>数据时间。大于等于</summary>
+ [DisplayName("数据时间")]
+ [Description("数据时间。大于等于")]
[DataObjectField(false, false, true, 0)]
- [BindColumn("Start", "开始。大于等于", "")]
- public DateTime Start { get => _Start; set { if (OnPropertyChanging("Start", value)) { _Start = value; OnPropertyChanged("Start"); } } }
+ [BindColumn("DataTime", "数据时间。大于等于", "")]
+ public DateTime DataTime { get => _DataTime; set { if (OnPropertyChanging("DataTime", value)) { _DataTime = value; OnPropertyChanged("DataTime"); } } }
private DateTime _End;
/// <summary>结束。小于,不等于</summary>
@@ -170,7 +170,7 @@ public partial class JobError
"JobID" => _JobID,
"TaskID" => _TaskID,
"Client" => _Client,
- "Start" => _Start,
+ "DataTime" => _DataTime,
"End" => _End,
"Data" => _Data,
"Server" => _Server,
@@ -192,7 +192,7 @@ public partial class JobError
case "JobID": _JobID = value.ToInt(); break;
case "TaskID": _TaskID = value.ToInt(); break;
case "Client": _Client = Convert.ToString(value); break;
- case "Start": _Start = value.ToDateTime(); break;
+ case "DataTime": _DataTime = value.ToDateTime(); break;
case "End": _End = value.ToDateTime(); break;
case "Data": _Data = Convert.ToString(value); break;
case "Server": _Server = Convert.ToString(value); break;
@@ -232,10 +232,13 @@ public partial class JobError
/// <summary>作业项</summary>
[Map(nameof(TaskID), typeof(JobTask), "ID")]
- public String TaskStart => Task?.ToString();
+ public String TaskDataTime => Task?.ToString();
#endregion
+ #region 扩展查询
+ #endregion
+
#region 字段名
/// <summary>取得作业错误字段信息的快捷方式</summary>
public partial class _
@@ -255,8 +258,8 @@ public partial class JobError
/// <summary>客户端。IP加进程</summary>
public static readonly Field Client = FindByName("Client");
- /// <summary>开始。大于等于</summary>
- public static readonly Field Start = FindByName("Start");
+ /// <summary>数据时间。大于等于</summary>
+ public static readonly Field DataTime = FindByName("DataTime");
/// <summary>结束。小于,不等于</summary>
public static readonly Field End = FindByName("End");
@@ -309,8 +312,8 @@ public partial class JobError
/// <summary>客户端。IP加进程</summary>
public const String Client = "Client";
- /// <summary>开始。大于等于</summary>
- public const String Start = "Start";
+ /// <summary>数据时间。大于等于</summary>
+ public const String DataTime = "DataTime";
/// <summary>结束。小于,不等于</summary>
public const String End = "End";
diff --git "a/AntJob.Data/Entity/\344\275\234\344\270\232\344\273\273\345\212\241.Biz.cs" "b/AntJob.Data/Entity/\344\275\234\344\270\232\344\273\273\345\212\241.Biz.cs"
index 5694561..e8f6c7e 100644
--- "a/AntJob.Data/Entity/\344\275\234\344\270\232\344\273\273\345\212\241.Biz.cs"
+++ "b/AntJob.Data/Entity/\344\275\234\344\270\232\344\273\273\345\212\241.Biz.cs"
@@ -92,30 +92,42 @@ public partial class JobTask : EntityBase<JobTask>
return FindAll(_.JobID == jobid & _.CreateTime < createTime, _.CreateTime.Desc(), null, 0, 1).FirstOrDefault();
}
- /// <summary>根据作业、状态、开始查找</summary>
- /// <param name="jobId">作业</param>
+ /// <summary>根据应用、客户端、状态查找</summary>
+ /// <param name="appId">应用</param>
+ /// <param name="client">客户端</param>
/// <param name="status">状态</param>
- /// <param name="start">开始</param>
/// <returns>实体列表</returns>
- public static IList<JobTask> FindAllByJobIDAndStatusAndStart(Int32 jobId, JobStatus status, DateTime start)
+ public static IList<JobTask> FindAllByAppIDAndClientAndStatus(Int32 appId, String client, JobStatus status)
{
// 实体缓存
- if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.JobID == jobId && e.Status == status && e.Start == start);
+ if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.AppID == appId && e.Client.EqualIgnoreCase(client) && e.Status == status);
- return FindAll(_.JobID == jobId & _.Status == status & _.Start == start);
+ return FindAll(_.AppID == appId & _.Client == client & _.Status == status);
}
- /// <summary>根据应用、客户端、状态查找</summary>
- /// <param name="appId">应用</param>
- /// <param name="client">客户端</param>
+ /// <summary>根据作业、状态、数据时间查找</summary>
+ /// <param name="jobId">作业</param>
/// <param name="status">状态</param>
+ /// <param name="dataTime">数据时间</param>
/// <returns>实体列表</returns>
- public static IList<JobTask> FindAllByAppIDAndClientAndStatus(Int32 appId, String client, JobStatus status)
+ public static IList<JobTask> FindAllByJobIDAndStatusAndDataTime(Int32 jobId, JobStatus status, DateTime dataTime)
{
// 实体缓存
- if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.AppID == appId && e.Client.EqualIgnoreCase(client) && e.Status == status);
+ if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.JobID == jobId && e.Status == status && e.DataTime == dataTime);
- return FindAll(_.AppID == appId & _.Client == client & _.Status == status);
+ return FindAll(_.JobID == jobId & _.Status == status & _.DataTime == dataTime);
+ }
+
+ /// <summary>根据作业、数据时间查找</summary>
+ /// <param name="jobId">作业</param>
+ /// <param name="dataTime">数据时间</param>
+ /// <returns>实体列表</returns>
+ public static IList<JobTask> FindAllByJobIDAndDataTime(Int32 jobId, DateTime dataTime)
+ {
+ // 实体缓存
+ if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.JobID == jobId && e.DataTime == dataTime);
+
+ return FindAll(_.JobID == jobId & _.DataTime == dataTime);
}
#endregion
@@ -125,50 +137,54 @@ public partial class JobTask : EntityBase<JobTask>
/// <param name="appid"></param>
/// <param name="jobid"></param>
/// <param name="status"></param>
+ /// <param name="dataStart"></param>
+ /// <param name="dataEnd"></param>
/// <param name="start"></param>
/// <param name="end"></param>
/// <param name="client"></param>
/// <param name="key"></param>
/// <param name="p"></param>
/// <returns></returns>
- public static IEnumerable<JobTask> Search(Int32 id, Int32 appid, Int32 jobid, JobStatus status, DateTime start, DateTime end, String client, String key, PageParameter p)
+ public static IEnumerable<JobTask> Search(Int32 id, Int32 appid, Int32 jobid, JobStatus status, DateTime dataStart, DateTime dataEnd, DateTime start, DateTime end, String client, String key, PageParameter p)
{
var exp = new WhereExpression();
if (id > 0) exp &= _.ID == id;
- if (appid > 0) exp &= _.AppID == appid;
- if (jobid > 0) exp &= _.JobID == jobid;
+ if (appid >= 0) exp &= _.AppID == appid;
+ if (jobid >= 0) exp &= _.JobID == jobid;
if (status >= JobStatus.就绪) exp &= _.Status == status;
if (!client.IsNullOrEmpty()) exp &= _.Client == client;
if (!key.IsNullOrEmpty()) exp &= _.Data.Contains(key) | _.Message.Contains(key) | _.Key == key;
- exp &= _.Start.Between(start, end);
+
+ exp &= _.DataTime.Between(dataStart, dataEnd);
+ exp &= _.UpdateTime.Between(start, end);
return FindAll(exp, p);
}
/// <summary>获取该任务下特定状态的任务项</summary>
/// <param name="taskid"></param>
+ /// <param name="start"></param>
/// <param name="end"></param>
/// <param name="maxRetry"></param>
- /// <param name="stats"></param>
+ /// <param name="maxError"></param>
+ /// <param name="status"></param>
/// <param name="count">要申请的任务个数</param>
/// <returns></returns>
- public static IList<JobTask> Search(Int32 taskid, DateTime end, Int32 maxRetry, JobStatus[] stats, Int32 count)
+ public static IList<JobTask> Search(Int32 taskid, DateTime start, DateTime end, Int32 maxRetry, Int32 maxError, JobStatus[] status, Int32 count)
{
var exp = new WhereExpression();
if (taskid > 0) exp &= _.JobID == taskid;
if (maxRetry > 0) exp &= _.Times < maxRetry;
- exp &= _.Status.In(stats);
- exp &= _.UpdateTime >= DateTime.Now.AddDays(-7);
- if (end > DateTime.MinValue)
- {
- exp &= _.UpdateTime < end;
- }
+ if (status != null && status.Length > 0) exp &= _.Status.In(status);
// 限制任务的错误次数,避免无限执行
- exp &= _.Error < 32;
+ if (maxError > 0) exp &= _.Error < maxError;
- return FindAll(exp, _.ID.Asc(), null, 0, count);
+ if (start.Year > 2000) exp &= _.UpdateTime >= start;
+ if (end.Year > 2000) exp &= _.UpdateTime < end;
+
+ return FindAll(exp, _.UpdateTime.Asc(), null, 0, count);
}
#endregion
@@ -191,6 +207,10 @@ public partial class JobTask : EntityBase<JobTask>
//public static Int32 DeleteByAppId(Int32 appid) => Delete(_.AppID == appid);
+ /// <summary>删除作业已不存在的任务</summary>
+ /// <returns></returns>
+ public static Int32 DeleteNoJob() => Delete(_.JobID.NotIn(Entity.Job.FindSQLWithKey()));
+
/// <summary>转模型类</summary>
/// <returns></returns>
public TaskModel ToModel()
@@ -199,7 +219,7 @@ public partial class JobTask : EntityBase<JobTask>
{
ID = ID,
- Start = Start,
+ DataTime = DataTime,
End = End,
//Offset = Offset,
//Step = Step,
diff --git "a/AntJob.Data/Entity/\344\275\234\344\270\232\344\273\273\345\212\241.cs" "b/AntJob.Data/Entity/\344\275\234\344\270\232\344\273\273\345\212\241.cs"
index c0f1236..d9ad2ff 100644
--- "a/AntJob.Data/Entity/\344\275\234\344\270\232\344\273\273\345\212\241.cs"
+++ "b/AntJob.Data/Entity/\344\275\234\344\270\232\344\273\273\345\212\241.cs"
@@ -17,9 +17,10 @@ namespace AntJob.Data.Entity;
[Serializable]
[DataObject]
[Description("作业任务。计算作业在执行过程中生成的任务实例,具有该次执行所需参数")]
-[BindIndex("IX_JobTask_JobID_Status_Start", false, "JobID,Status,Start")]
-[BindIndex("IX_JobTask_AppID_Client_Status", false, "AppID,Client,Status")]
+[BindIndex("IX_JobTask_JobID_DataTime", false, "JobID,DataTime")]
[BindIndex("IX_JobTask_JobID_CreateTime", false, "JobID,CreateTime")]
+[BindIndex("IX_JobTask_JobID_UpdateTime", false, "JobID,UpdateTime")]
+[BindIndex("IX_JobTask_AppID_Client_Status", false, "AppID,Client,Status")]
[BindTable("JobTask", Description = "作业任务。计算作业在执行过程中生成的任务实例,具有该次执行所需参数", ConnName = "Ant", DbType = DatabaseType.None)]
public partial class JobTask
{
@@ -56,13 +57,13 @@ public partial class JobTask
[BindColumn("Client", "客户端。IP加进程", "")]
public String Client { get => _Client; set { if (OnPropertyChanging("Client", value)) { _Client = value; OnPropertyChanged("Client"); } } }
- private DateTime _Start;
- /// <summary>开始。大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用</summary>
- [DisplayName("开始")]
- [Description("开始。大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用")]
+ private DateTime _DataTime;
+ /// <summary>数据时间。大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用</summary>
+ [DisplayName("数据时间")]
+ [Description("数据时间。大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用")]
[DataObjectField(false, false, true, 0)]
- [BindColumn("Start", "开始。大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用", "", Master = true)]
- public DateTime Start { get => _Start; set { if (OnPropertyChanging("Start", value)) { _Start = value; OnPropertyChanged("Start"); } } }
+ [BindColumn("DataTime", "数据时间。大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用", "", DataScale = "time", Master = true)]
+ public DateTime DataTime { get => _DataTime; set { if (OnPropertyChanging("DataTime", value)) { _DataTime = value; OnPropertyChanged("DataTime"); } } }
private DateTime _End;
/// <summary>结束。小于不等于,数据调度到达该时间点后触发(可能有偏移量),消息调度不适用</summary>
@@ -125,7 +126,7 @@ public partial class JobTask
[DisplayName("耗时")]
[Description("耗时。秒,执行端计算的执行时间")]
[DataObjectField(false, false, false, 0)]
- [BindColumn("Cost", "耗时。秒,执行端计算的执行时间", "")]
+ [BindColumn("Cost", "耗时。秒,执行端计算的执行时间", "", ItemType = "TimeSpan")]
public Int32 Cost { get => _Cost; set { if (OnPropertyChanging("Cost", value)) { _Cost = value; OnPropertyChanged("Cost"); } } }
private Int32 _FullCost;
@@ -133,7 +134,7 @@ public partial class JobTask
[DisplayName("全部耗时")]
[Description("全部耗时。秒,从任务发放到执行完成的时间")]
[DataObjectField(false, false, false, 0)]
- [BindColumn("FullCost", "全部耗时。秒,从任务发放到执行完成的时间", "")]
+ [BindColumn("FullCost", "全部耗时。秒,从任务发放到执行完成的时间", "", ItemType = "TimeSpan")]
public Int32 FullCost { get => _FullCost; set { if (OnPropertyChanging("FullCost", value)) { _FullCost = value; OnPropertyChanged("FullCost"); } } }
private JobStatus _Status;
@@ -145,11 +146,11 @@ public partial class JobTask
public JobStatus Status { get => _Status; set { if (OnPropertyChanging("Status", value)) { _Status = value; OnPropertyChanged("Status"); } } }
private Int32 _MsgCount;
- /// <summary>消费消息数</summary>
- [DisplayName("消费消息数")]
- [Description("消费消息数")]
+ /// <summary>消息。消费消息数</summary>
+ [DisplayName("消息")]
+ [Description("消息。消费消息数")]
[DataObjectField(false, false, false, 0)]
- [BindColumn("MsgCount", "消费消息数", "")]
+ [BindColumn("MsgCount", "消息。消费消息数", "")]
public Int32 MsgCount { get => _MsgCount; set { if (OnPropertyChanging("MsgCount", value)) { _MsgCount = value; OnPropertyChanged("MsgCount"); } } }
private String _Server;
@@ -250,7 +251,7 @@ public partial class JobTask
"AppID" => _AppID,
"JobID" => _JobID,
"Client" => _Client,
- "Start" => _Start,
+ "DataTime" => _DataTime,
"End" => _End,
"BatchSize" => _BatchSize,
"Total" => _Total,
@@ -282,7 +283,7 @@ public partial class JobTask
case "AppID": _AppID = value.ToInt(); break;
case "JobID": _JobID = value.ToInt(); break;
case "Client": _Client = Convert.ToString(value); break;
- case "Start": _Start = value.ToDateTime(); break;
+ case "DataTime": _DataTime = value.ToDateTime(); break;
case "End": _End = value.ToDateTime(); break;
case "BatchSize": _BatchSize = value.ToInt(); break;
case "Total": _Total = value.ToInt(); break;
@@ -329,6 +330,51 @@ public partial class JobTask
#endregion
+ #region 扩展查询
+ /// <summary>根据编号查找</summary>
+ /// <param name="id">编号</param>
+ /// <returns>实体对象</returns>
+ public static JobTask FindByID(Int32 id)
+ {
+ if (id < 0) return null;
+
+ return Find(_.ID == id);
+ }
+
+ /// <summary>根据作业查找</summary>
+ /// <param name="jobId">作业</param>
+ /// <returns>实体列表</returns>
+ public static IList<JobTask> FindAllByJobID(Int32 jobId)
+ {
+ if (jobId < 0) return [];
+
+ return FindAll(_.JobID == jobId);
+ }
+
+ /// <summary>根据数据时间查找</summary>
+ /// <param name="dataTime">数据时间</param>
+ /// <returns>实体列表</returns>
+ public static IList<JobTask> FindAllByDataTime(DateTime dataTime)
+ {
+ if (dataTime.Year < 1000) return [];
+
+ return FindAll(_.DataTime == dataTime);
+ }
+ #endregion
+
+ #region 数据清理
+ /// <summary>清理指定时间段内的数据</summary>
+ /// <param name="start">开始时间。未指定时清理小于指定时间的所有数据</param>
+ /// <param name="end">结束时间</param>
+ /// <returns>清理行数</returns>
+ public static Int32 DeleteWith(DateTime start, DateTime end)
+ {
+ if (start == end) return Delete(_.DataTime == start);
+
+ return Delete(_.DataTime.Between(start, end));
+ }
+ #endregion
+
#region 字段名
/// <summary>取得作业任务字段信息的快捷方式</summary>
public partial class _
@@ -345,8 +391,8 @@ public partial class JobTask
/// <summary>客户端。IP加进程</summary>
public static readonly Field Client = FindByName("Client");
- /// <summary>开始。大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用</summary>
- public static readonly Field Start = FindByName("Start");
+ /// <summary>数据时间。大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用</summary>
+ public static readonly Field DataTime = FindByName("DataTime");
/// <summary>结束。小于不等于,数据调度到达该时间点后触发(可能有偏移量),消息调度不适用</summary>
public static readonly Field End = FindByName("End");
@@ -378,7 +424,7 @@ public partial class JobTask
/// <summary>状态</summary>
public static readonly Field Status = FindByName("Status");
- /// <summary>消费消息数</summary>
+ /// <summary>消息。消费消息数</summary>
public static readonly Field MsgCount = FindByName("MsgCount");
/// <summary>服务器</summary>
@@ -429,8 +475,8 @@ public partial class JobTask
/// <summary>客户端。IP加进程</summary>
public const String Client = "Client";
- /// <summary>开始。大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用</summary>
- public const String Start = "Start";
+ /// <summary>数据时间。大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用</summary>
+ public const String DataTime = "DataTime";
/// <summary>结束。小于不等于,数据调度到达该时间点后触发(可能有偏移量),消息调度不适用</summary>
public const String End = "End";
@@ -462,7 +508,7 @@ public partial class JobTask
/// <summary>状态</summary>
public const String Status = "Status";
- /// <summary>消费消息数</summary>
+ /// <summary>消息。消费消息数</summary>
public const String MsgCount = "MsgCount";
/// <summary>服务器</summary>
diff --git a/AntJob.Data/Model.xml b/AntJob.Data/Model.xml
index 4f57e08..fa1eb2b 100644
--- a/AntJob.Data/Model.xml
+++ b/AntJob.Data/Model.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<EntityModel xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" xs:schemaLocation="https://newlifex.com https://newlifex.com/Model202309.xsd" Document="https://newlifex.com/xcode/model" xmlns="https://newlifex.com/Model202309.xsd">
+<EntityModel xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" xs:schemaLocation="https://newlifex.com https://newlifex.com/Model202407.xsd" Document="https://newlifex.com/xcode/model" xmlns="https://newlifex.com/Model202407.xsd">
<Option>
<!--类名模板。其中{name}替换为Table.Name,如{name}Model/I{name}Dto等-->
<ClassNameTemplate />
@@ -42,15 +42,17 @@
<Table Name="App" Description="应用系统。管理数据计算作业的应用模块,计算作业隶属于某个应用">
<Columns>
<Column Name="ID" DataType="Int32" Identity="True" PrimaryKey="True" Description="编号" />
- <Column Name="Name" DataType="String" Master="True" Nullable="False" Description="名称" />
- <Column Name="DisplayName" DataType="String" Description="显示名" />
- <Column Name="Secret" DataType="String" Description="密钥" />
+ <Column Name="Name" DataType="String" Master="True" Nullable="False" Description="名称。应用英文名" />
+ <Column Name="DisplayName" DataType="String" Description="显示名。应用中文名" />
+ <Column Name="Secret" DataType="String" Description="密钥。一般不设置,应用默认接入" />
<Column Name="Category" DataType="String" Description="类别" />
<Column Name="Version" DataType="String" Description="版本" />
<Column Name="CompileTime" DataType="DateTime" Description="编译时间" />
<Column Name="Enable" DataType="Boolean" Description="启用" />
- <Column Name="JobCount" DataType="Int32" Description="作业数" />
- <Column Name="MessageCount" DataType="Int32" Description="消息数" />
+ <Column Name="JobCount" DataType="Int32" Description="作业数。该应用下作业个数" />
+ <Column Name="MessageCount" DataType="Int32" Description="消息数。该应用下消息条数" />
+ <Column Name="ManagerId" DataType="Int32" Map="XCode.Membership.User@ID@Name" Description="管理人。负责该应用的管理员" />
+ <Column Name="Manager" DataType="String" Description="管理者" />
<Column Name="CreateUserID" DataType="Int32" Description="创建人" Category="扩展" />
<Column Name="CreateUser" DataType="String" Description="创建者" Category="扩展" />
<Column Name="CreateTime" DataType="DateTime" Description="创建时间" Category="扩展" />
@@ -76,11 +78,12 @@
<Column Name="Version" DataType="String" Description="版本。客户端" />
<Column Name="CompileTime" DataType="DateTime" Description="编译时间" />
<Column Name="Server" DataType="String" Description="服务端。客户端登录到哪个服务端,IP加端口" />
+ <Column Name="Enable" DataType="Boolean" Description="启用。是否允许申请任务" />
<Column Name="Tasks" DataType="Int32" Description="任务数" />
<Column Name="Total" DataType="Int64" Description="总数" />
<Column Name="Success" DataType="Int64" Description="成功" />
<Column Name="Error" DataType="Int64" Description="错误" />
- <Column Name="Cost" DataType="Int64" Description="耗时。执行任务总耗时,秒" />
+ <Column Name="Cost" DataType="Int64" ItemType="TimeSpan" Description="耗时。执行任务总耗时,秒" />
<Column Name="Speed" DataType="Int64" Description="速度。每秒处理数" />
<Column Name="LastKey" DataType="String" Description="最后键" />
<Column Name="TraceId" DataType="String" Length="200" Description="追踪。链路追踪,用于APM性能追踪定位,还原该事件的调用链" Category="扩展" />
@@ -97,7 +100,7 @@
</Table>
<Table Name="AppHistory" Description="应用历史。数据计算应用的操作历史">
<Columns>
- <Column Name="Id" DataType="Int64" PrimaryKey="True" Description="编号" />
+ <Column Name="Id" DataType="Int64" PrimaryKey="True" DataScale="time" Description="编号" />
<Column Name="AppID" DataType="Int32" Map="App@ID@$" Description="应用" />
<Column Name="Name" DataType="String" Master="True" Description="名称" />
<Column Name="Action" DataType="String" Description="操作" />
@@ -114,52 +117,38 @@
<Index Columns="AppID,Action" />
</Indexes>
</Table>
- <Table Name="AppConfig" Description="应用配置。数据计算应用的配置数据">
- <Columns>
- <Column Name="ID" DataType="Int32" Identity="True" PrimaryKey="True" Description="编号" />
- <Column Name="AppID" DataType="Int32" Map="App@ID@$" Description="应用" />
- <Column Name="Name" DataType="String" Master="True" Description="名称" />
- <Column Name="Content" DataType="String" Length="5000" Description="内容。一般是json格式" />
- <Column Name="CreateUserID" DataType="Int32" Description="创建人" Category="扩展" />
- <Column Name="CreateTime" DataType="DateTime" Description="创建时间" Category="扩展" />
- <Column Name="CreateIP" DataType="String" Description="创建地址" Category="扩展" />
- <Column Name="UpdateUserID" DataType="Int32" Description="更新人" Category="扩展" />
- <Column Name="UpdateTime" DataType="DateTime" Description="更新时间" Category="扩展" />
- <Column Name="UpdateIP" DataType="String" Description="更新地址" Category="扩展" />
- </Columns>
- <Indexes>
- <Index Columns="AppID,Name" Unique="True" />
- <Index Columns="UpdateTime" />
- </Indexes>
- </Table>
- <Table Name="Job" Description="作业。数据计算逻辑的主要单元,每个作业即是一个业务逻辑,各个作业之前存在依赖关系">
+ <Table Name="Job" Description="作业。数据计算逻辑的主要单元,每个作业即是一个业务逻辑">
<Columns>
<Column Name="ID" DataType="Int32" Identity="True" PrimaryKey="True" Description="编号" />
<Column Name="AppID" DataType="Int32" Map="App@ID@$" Description="应用" />
<Column Name="Name" DataType="String" Master="True" Length="100" Description="名称" />
<Column Name="ClassName" DataType="String" Length="100" Description="类名。支持该作业的处理器实现" />
<Column Name="DisplayName" DataType="String" Description="显示名" />
+ <Column Name="Enable" DataType="Boolean" Description="启用" />
<Column Name="Mode" DataType="Int32" Description="调度模式。定时调度只要达到时间片开头就可以跑,数据调度要求达到时间片末尾才可以跑" Type="JobModes" />
+ <Column Name="Cron" DataType="String" Description="执行频次。定时调度的Cron表达式" />
<Column Name="Topic" DataType="String" Description="主题。消息调度时消费的主题" />
<Column Name="MessageCount" DataType="Int32" Description="消息数" />
- <Column Name="Start" DataType="DateTime" Description="开始。大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用" />
- <Column Name="End" DataType="DateTime" Description="结束。小于不等于,数据调度到达该时间点后触发(可能有偏移量),默认空表示无止境,消息调度不适用" />
- <Column Name="Step" DataType="Int32" Description="步进。切分任务的时间区间,秒" />
+ <Column Name="DataTime" DataType="DateTime" Description="数据时间。下一次处理数据的时间,默认从当前时间开始" />
+ <Column Name="End" DataType="DateTime" Description="结束。到该时间停止调度作业,默认不设置时永不停止" />
+ <Column Name="Step" DataType="Int32" ItemType="TimeSpan" Description="步进。切分任务的时间区间,秒" />
<Column Name="BatchSize" DataType="Int32" Description="批大小。数据调度每次抽取数据的分页大小,或消息调度每次处理的消息数,定时调度不适用" />
- <Column Name="Offset" DataType="Int32" Description="偏移。距离AntServer当前时间的秒数,避免因服务器之间的时间误差而错过部分数据,秒" />
- <Column Name="MaxTask" DataType="Int32" Description="并行度。一共允许多少个任务并行处理,多执行端时平均分配,确保该作业整体并行度" />
- <Column Name="MaxError" DataType="Int32" Description="最大错误。连续错误达到最大错误数时停止" />
- <Column Name="MaxRetry" DataType="Int32" Description="最大重试。默认10次,超过该次数后将不再重试" />
- <Column Name="MaxTime" DataType="Int32" Description="最大执行时间。默认600秒,超过该时间则认为执行器故障,将会把该任务分配给其它执行器" />
- <Column Name="MaxRetain" DataType="Int32" Description="保留。任务项保留天数,超过天数的任务项将被删除,默认3天" />
- <Column Name="MaxIdle" DataType="Int32" Description="最大空闲时间。默认3600秒,超过该时间不更新则认为应用程序故障,系统触发告警" />
- <Column Name="ErrorDelay" DataType="Int32" Description="错误延迟。默认60秒,出错延迟后重新发放" />
- <Column Name="Total" DataType="Int64" Description="总数。任务处理的总数据,例如数据调度抽取得到的总行数,定时调度默认1" />
- <Column Name="Success" DataType="Int64" Description="成功。成功处理的数据,取自于Handler.Execute返回值,或者ProcessItem返回true的个数" />
- <Column Name="Error" DataType="Int32" Description="错误" />
- <Column Name="Times" DataType="Int32" Description="次数" />
- <Column Name="Speed" DataType="Int32" Description="速度" />
- <Column Name="Enable" DataType="Boolean" Description="启用" />
+ <Column Name="Offset" DataType="Int32" ItemType="TimeSpan" Description="偏移。距离AntServer当前时间的秒数,避免因服务器之间的时间误差而错过部分数据,秒" />
+ <Column Name="MaxTask" DataType="Int32" Description="并行度。一共允许多少个任务并行处理,多执行端时平均分配,确保该作业整体并行度" Category="控制参数" />
+ <Column Name="MaxError" DataType="Int32" Description="最大错误。连续错误达到最大错误数时停止" Category="控制参数" />
+ <Column Name="MaxRetry" DataType="Int32" Description="最大重试。默认10次,超过该次数后将不再重试" Category="控制参数" />
+ <Column Name="MaxTime" DataType="Int32" ItemType="TimeSpan" Description="最大执行时间。默认600秒,超过该时间则认为执行器故障,将会把该任务分配给其它执行器" Category="控制参数" />
+ <Column Name="MaxRetain" DataType="Int32" Description="保留。任务项保留天数,超过天数的任务项将被删除,默认30天" Category="控制参数" />
+ <Column Name="MaxIdle" DataType="Int32" ItemType="TimeSpan" Description="最大空闲时间。默认3600秒,超过该时间不更新则认为应用程序故障,系统触发告警" Category="控制参数" />
+ <Column Name="ErrorDelay" DataType="Int32" ItemType="TimeSpan" Description="错误延迟。默认60秒,出错延迟后重新发放" Category="控制参数" />
+ <Column Name="Deadline" DataType="DateTime" Description="最后期限。超过该时间后,任务将不再执行" Category="控制参数" />
+ <Column Name="Total" DataType="Int64" Description="总数。任务处理的总数据,例如数据调度抽取得到的总行数,定时调度默认1" Category="统计" />
+ <Column Name="Success" DataType="Int64" Description="成功。成功处理的数据,取自于Handler.Execute返回值,或者ProcessItem返回true的个数" Category="统计" />
+ <Column Name="Error" DataType="Int32" Description="错误" Category="统计" />
+ <Column Name="Times" DataType="Int32" Description="次数" Category="统计" />
+ <Column Name="Speed" DataType="Int32" Description="速度" Category="统计" />
+ <Column Name="LastStatus" DataType="Int32" Description="最后状态。最后一次状态" Category="统计" Type="JobStatus" />
+ <Column Name="LastTime" DataType="DateTime" Description="最后时间。最后一次时间" Category="统计" />
<Column Name="Data" DataType="String" Length="-1" Description="数据。Sql模板或C#模板" />
<Column Name="CreateUserID" DataType="Int32" Description="创建人" Category="扩展" />
<Column Name="CreateUser" DataType="String" Description="创建者" Category="扩展" />
@@ -181,7 +170,7 @@
<Column Name="AppID" DataType="Int32" Map="App@ID@$" Description="应用" />
<Column Name="JobID" DataType="Int32" Map="Job@ID@$" Description="作业" />
<Column Name="Client" DataType="String" Description="客户端。IP加进程" />
- <Column Name="Start" DataType="DateTime" Master="True" Description="开始。大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用" />
+ <Column Name="DataTime" DataType="DateTime" Master="True" DataScale="time" Description="数据时间。大于等于,定时调度到达该时间点后触发(可能有偏移量),消息调度不适用" />
<Column Name="End" DataType="DateTime" Description="结束。小于不等于,数据调度到达该时间点后触发(可能有偏移量),消息调度不适用" />
<Column Name="BatchSize" DataType="Int32" Description="批大小。数据调度每次抽取数据的分页大小,或消息调度每次处理的消息数,定时调度不适用" />
<Column Name="Total" DataType="Int32" Description="总数。任务处理的总数据,例如数据调度抽取得到的总行数,定时调度默认1" />
@@ -189,10 +178,10 @@
<Column Name="Error" DataType="Int32" Description="错误" />
<Column Name="Times" DataType="Int32" Description="次数" />
<Column Name="Speed" DataType="Int32" Description="速度。每秒处理数,执行端计算" />
- <Column Name="Cost" DataType="Int32" Description="耗时。秒,执行端计算的执行时间" />
- <Column Name="FullCost" DataType="Int32" Description="全部耗时。秒,从任务发放到执行完成的时间" />
+ <Column Name="Cost" DataType="Int32" ItemType="TimeSpan" Description="耗时。秒,执行端计算的执行时间" />
+ <Column Name="FullCost" DataType="Int32" ItemType="TimeSpan" Description="全部耗时。秒,从任务发放到执行完成的时间" />
<Column Name="Status" DataType="Int32" Description="状态" Type="JobStatus" />
- <Column Name="MsgCount" DataType="Int32" Description="消费消息数" />
+ <Column Name="MsgCount" DataType="Int32" Description="消息。消费消息数" />
<Column Name="Server" DataType="String" Description="服务器" />
<Column Name="ProcessID" DataType="Int32" Description="进程" />
<Column Name="Key" DataType="String" Description="最后键。Handler内记录作为样本的数据" />
@@ -205,9 +194,10 @@
<Column Name="UpdateTime" DataType="DateTime" Description="更新时间" Category="扩展" />
</Columns>
<Indexes>
- <Index Columns="JobID,Status,Start" />
- <Index Columns="AppID,Client,Status" />
+ <Index Columns="JobID,DataTime" />
<Index Columns="JobID,CreateTime" />
+ <Index Columns="JobID,UpdateTime" />
+ <Index Columns="AppID,Client,Status" />
</Indexes>
</Table>
<Table Name="JobError" Description="作业错误。计算作业在执行过程中所发生的错误">
@@ -217,7 +207,7 @@
<Column Name="JobID" DataType="Int32" Map="Job@ID@$" Description="作业" />
<Column Name="TaskID" DataType="Int32" Map="JobTask@ID@$" Description="作业项" />
<Column Name="Client" DataType="String" Description="客户端。IP加进程" />
- <Column Name="Start" DataType="DateTime" Description="开始。大于等于" />
+ <Column Name="DataTime" DataType="DateTime" Description="数据时间。大于等于" />
<Column Name="End" DataType="DateTime" Description="结束。小于,不等于" />
<Column Name="Data" DataType="String" Length="2000" Description="数据" />
<Column Name="Server" DataType="String" Description="服务器" />
@@ -236,11 +226,12 @@
</Table>
<Table Name="AppMessage" Description="应用消息。消息调度,某些作业负责生产消息,供其它作业进行消费处理">
<Columns>
- <Column Name="Id" DataType="Int64" PrimaryKey="True" Description="编号" />
+ <Column Name="Id" DataType="Int64" PrimaryKey="True" DataScale="time" Description="编号" />
<Column Name="AppID" DataType="Int32" Map="App@ID@$" Description="应用" />
<Column Name="JobID" DataType="Int32" Map="Job@ID@$" Description="作业。生产消息的作业" />
<Column Name="Topic" DataType="String" Description="主题。区分作业下多种消息" />
<Column Name="Data" DataType="String" Length="2000" Description="数据。可以是Json数据,比如StatID" />
+ <Column Name="DelayTime" DataType="DateTime" Description="延迟时间。延迟到该时间执行" />
<Column Name="TraceId" DataType="String" Length="200" Description="追踪。链路追踪,用于APM性能追踪定位,还原该事件的调用链" Category="扩展" />
<Column Name="CreateIP" DataType="String" Description="创建地址" Category="扩展" />
<Column Name="CreateTime" DataType="DateTime" Description="创建时间" Category="扩展" />
@@ -249,6 +240,7 @@
</Columns>
<Indexes>
<Index Columns="AppID,Topic,UpdateTime" />
+ <Index Columns="AppID,Topic,DelayTime" />
</Indexes>
</Table>
</Tables>
diff --git a/AntJob.Data/xcodetool.bat b/AntJob.Data/xcodetool.bat
new file mode 100644
index 0000000..704a9a3
--- /dev/null
+++ b/AntJob.Data/xcodetool.bat
@@ -0,0 +1,5 @@
+xcode
+IF ERRORLEVEL 1 (
+ echo xcode ����ִ��ʧ�ܣ����ڰ�װ .NET ����...
+ dotnet tool install xcodetool -g --prerelease
+)
\ No newline at end of file
diff --git a/AntJob.Data/xcodetool.exe b/AntJob.Data/xcodetool.exe
index 262a2aa..a69ad0d 100644
Binary files a/AntJob.Data/xcodetool.exe and b/AntJob.Data/xcodetool.exe differ
diff --git a/AntJob.Extensions/AntJob.Extensions.csproj b/AntJob.Extensions/AntJob.Extensions.csproj
index 45dc81e..d07f05d 100644
--- a/AntJob.Extensions/AntJob.Extensions.csproj
+++ b/AntJob.Extensions/AntJob.Extensions.csproj
@@ -43,7 +43,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="NewLife.XCode" Version="11.11.2024.402" />
+ <PackageReference Include="NewLife.XCode" Version="11.16.2024.1005" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AntJob\AntJob.csproj" />
diff --git a/AntJob.Extensions/DataHandler.cs b/AntJob.Extensions/DataHandler.cs
index dddf179..2e84a81 100644
--- a/AntJob.Extensions/DataHandler.cs
+++ b/AntJob.Extensions/DataHandler.cs
@@ -1,6 +1,7 @@
using System.Collections;
using AntJob.Data;
using NewLife;
+using NewLife.Log;
using XCode;
using XCode.Configuration;
@@ -8,24 +9,57 @@ namespace AntJob.Extensions;
/// <summary>数据处理作业(泛型)</summary>
/// <remarks>
+/// 文档:https://newlifex.com/blood/antjob
+///
/// 定时调度只要达到时间片开头就可以跑,数据调度要求达到时间片末尾才可以跑。
-/// 任务切片条件:StartTime + Step + Offset <= Now
+/// 任务切片条件:DataTime + Step + Offset <= Now
/// </remarks>
/// <typeparam name="TEntity"></typeparam>
public abstract class DataHandler<TEntity> : DataHandler where TEntity : Entity<TEntity>, new()
{
/// <summary>实例化数据处理作业</summary>
- public DataHandler() => Factory = Entity<TEntity>.Meta.Factory;
+ public DataHandler()
+ {
+ Factory = Entity<TEntity>.Meta.Factory;
+
+ // 自动识别数据分区字段、主时间字段、雪花Id、更新时间字段、创建时间字段
+ var fact = Factory;
+ var field = fact.Fields.FirstOrDefault(e => e.Field != null && e.Field.DataScale.StartsWithIgnoreCase("time", "timeShard"));
+ field ??= fact.Fields.FirstOrDefault(e => e.PrimaryKey && !e.IsIdentity && e.Type == typeof(Int64));
+ field ??= fact.MasterTime;
+ field ??= fact.Fields.FirstOrDefault(e => e.Name.EqualIgnoreCase("UpdateTime"));
+ field ??= fact.Fields.FirstOrDefault(e => e.Name.EqualIgnoreCase("CreateTime"));
+
+ Field = field;
+ }
#region 数据处理
+ /// <summary>分批抽取数据,一个任务内多次调用</summary>
+ /// <param name="ctx">上下文</param>
+ /// <param name="row">开始行数</param>
+ /// <returns></returns>
+ protected override Object Fetch(JobContext ctx, ref Int32 row)
+ {
+ var list = base.Fetch(ctx, ref row);
+ if (list is IEnumerable enumerable)
+ {
+ // 修改列表类型,由 IList<IEntity> 改为 IList<TEntity> ,方便用户使用
+ list = enumerable.Cast<TEntity>().ToList();
+ }
+
+ return list;
+ }
+
/// <summary>处理一批数据</summary>
/// <param name="ctx">上下文</param>
/// <returns></returns>
- protected override Int32 Execute(JobContext ctx)
+ public override Int32 Execute(JobContext ctx)
{
var count = 0;
foreach (var item in ctx.Data as IEnumerable)
+ {
if (ProcessItem(ctx, item as TEntity)) count++;
+ }
return count;
}
@@ -34,14 +68,16 @@ public abstract class DataHandler<TEntity> : DataHandler where TEntity : Entity<
/// <param name="ctx">上下文</param>
/// <param name="entity"></param>
/// <returns></returns>
- protected virtual Boolean ProcessItem(JobContext ctx, TEntity entity) => true;
+ public virtual Boolean ProcessItem(JobContext ctx, TEntity entity) => true;
#endregion
}
/// <summary>数据处理作业</summary>
/// <remarks>
+/// 文档:https://newlifex.com/blood/antjob
+///
/// 定时调度只要达到时间片开头就可以跑,数据调度要求达到时间片末尾才可以跑。
-/// 任务切片条件:StartTime + Step + Offset <= Now
+/// 任务切片条件:DataTime + Step + Offset <= Now
/// </remarks>
public abstract class DataHandler : Handler
{
@@ -60,6 +96,9 @@ public abstract class DataHandler : Handler
/// <summary>选择列</summary>
public String Selects { get; set; }
+
+ /// <summary>每次都只查第一页</summary>
+ public bool KeepFirstPage { get; set; }
#endregion
#region 构造
@@ -68,11 +107,9 @@ public abstract class DataHandler : Handler
#endregion
#region 方法
- /// <summary>开始</summary>
- public override Boolean Start()
+ /// <summary>初始化。作业处理器启动之前</summary>
+ public override void Init()
{
- if (Active) return false;
-
if (Factory == null) throw new ArgumentNullException(nameof(Factory));
// 自动识别雪花Id字段
@@ -87,10 +124,38 @@ public abstract class DataHandler : Handler
var job = Job;
if (job.Step == 0) job.Step = 30;
- // 获取最小时间
- if (job.Start.Year < 2000) throw new InvalidOperationException("数据任务必须设置开始时间");
+ //todo 如果DataTime为空,则自动获取最小时间,并设置到DataTime,以减轻平台设置负担
- return base.Start();
+ // 获取最小数据时间
+ if (job.DataTime.Year < 2000)
+ {
+ //throw new InvalidOperationException("数据任务必须设置开始时间");
+ job.DataTime = GetMinDataTime();
+ }
+ }
+
+ /// <summary>获取最小数据时间。初始化作业时自动设置首个数据时间</summary>
+ /// <returns></returns>
+ public virtual DateTime GetMinDataTime()
+ {
+ var field = Field;
+ if (field == null) return DateTime.MinValue;
+
+ // 按时间字段升序,取第一个
+ var list = Factory.FindAll(null, field.Asc(), field, 0, 1);
+ if (list.Count > 0)
+ {
+ var value = list[0][field.Name];
+ if (field.Type == typeof(Int64))
+ {
+ // 雪花Id
+ return Factory.Snow.TryParse(value.ToLong(), out var dt, out _, out _) ? dt.ToLocalTime() : DateTime.MinValue;
+ }
+
+ return value.ToDateTime();
+ }
+
+ return DateTime.MinValue;
}
#endregion
@@ -99,7 +164,7 @@ public abstract class DataHandler : Handler
/// <param name="ctx"></param>
protected override void OnProcess(JobContext ctx)
{
- var prov = Provider;
+ var span = DefaultSpan.Current;
var row = 0;
while (true)
{
@@ -111,14 +176,17 @@ public abstract class DataHandler : Handler
var data = Fetch(ctx, ref row);
var list = data as IList;
- if (list != null) ctx.Total += list.Count;
+ if (list != null)
+ {
+ ctx.Total += list.Count;
+ if (span != null) span.Value = ctx.Total;
+ }
ctx.Data = data;
if (data == null || list != null && list.Count == 0) break;
// 报告进度
- ctx.Status = JobStatus.处理中;
- prov?.Report(ctx);
+ Report(ctx, JobStatus.处理中);
// 批量处理
ctx.Success += Execute(ctx);
@@ -141,7 +209,7 @@ public abstract class DataHandler : Handler
if (task == null) throw new ArgumentNullException(nameof(task), "没有设置数据抽取配置");
// 验证时间段
- var start = task.Start;
+ var start = task.DataTime;
var end = task.End;
// 区间无效
@@ -166,7 +234,7 @@ public abstract class DataHandler : Handler
if (!Where.IsNullOrEmpty()) exp &= Where;
- var list = Factory.FindAll(exp, OrderBy, Selects, row, task.BatchSize);
+ var list = Factory.FindAll(exp, OrderBy, Selects, this.KeepFirstPage ? 0 : row, task.BatchSize);
// 取到数据,需要滑动窗口
if (list.Count > 0) row += list.Count;
@@ -177,11 +245,13 @@ public abstract class DataHandler : Handler
/// <summary>处理一批数据</summary>
/// <param name="ctx">上下文</param>
/// <returns></returns>
- protected override Int32 Execute(JobContext ctx)
+ public override Int32 Execute(JobContext ctx)
{
var count = 0;
foreach (var item in ctx.Data as IEnumerable)
+ {
if (ProcessItem(ctx, item as IEntity)) count++;
+ }
return count;
}
@@ -190,6 +260,6 @@ public abstract class DataHandler : Handler
/// <param name="ctx">上下文</param>
/// <param name="entity"></param>
/// <returns></returns>
- protected virtual Boolean ProcessItem(JobContext ctx, IEntity entity) => true;
+ public virtual Boolean ProcessItem(JobContext ctx, IEntity entity) => true;
#endregion
}
\ No newline at end of file
diff --git a/AntJob.Extensions/SqlHandler.cs b/AntJob.Extensions/SqlHandler.cs
index b502fe7..197aecb 100644
--- a/AntJob.Extensions/SqlHandler.cs
+++ b/AntJob.Extensions/SqlHandler.cs
@@ -24,11 +24,11 @@ public class SqlHandler : Handler
/// <summary>执行</summary>
/// <param name="ctx"></param>
/// <returns></returns>
- protected override Int32 Execute(JobContext ctx)
+ public override Int32 Execute(JobContext ctx)
{
- //var sqls = ctx.Task.Data as String;
- var sqls = Job.Data;
- sqls = TemplateHelper.Build(sqls, ctx.Task.Start, ctx.Task.End);
+ var sqls = ctx.Task.Data;
+ //var sqls = Job.Data;
+ sqls = TemplateHelper.Build(sqls, ctx.Task.DataTime, ctx.Task.End);
// 向调度中心返回解析后的Sql语句
ctx.Remark = sqls;
@@ -54,12 +54,17 @@ public class SqlHandler : Handler
// 打开事务
foreach (var item in sections)
- if (item.Action != SqlActions.Query) DAL.Create(item.ConnName).BeginTransaction();
+ {
+ if (item.Action != SqlActions.Query)
+ DAL.Create(item.ConnName).BeginTransaction();
+ }
+
try
{
// 按顺序执行处理Sql语句
DbTable dt = null;
foreach (var section in sections)
+ {
switch (section.Action)
{
case SqlActions.Query:
@@ -79,16 +84,23 @@ public class SqlHandler : Handler
default:
break;
}
+ }
// 提交事务
foreach (var item in sections)
- if (item.Action != SqlActions.Query) DAL.Create(item.ConnName).Commit();
+ {
+ if (item.Action != SqlActions.Query)
+ DAL.Create(item.ConnName).Commit();
+ }
}
catch
{
// 回滚事务
foreach (var item in sections)
- if (item.Action != SqlActions.Query) DAL.Create(item.ConnName).Rollback();
+ {
+ if (item.Action != SqlActions.Query)
+ DAL.Create(item.ConnName).Rollback();
+ }
throw;
}
diff --git a/AntJob.Extensions/SqlMessage.cs b/AntJob.Extensions/SqlMessage.cs
index 10ef7ce..29f26fd 100644
--- a/AntJob.Extensions/SqlMessage.cs
+++ b/AntJob.Extensions/SqlMessage.cs
@@ -20,7 +20,7 @@ public class SqlMessage : MessageHandler
/// <summary>根据解码后的消息执行任务</summary>
/// <param name="ctx">上下文</param>
/// <returns></returns>
- protected override Int32 Execute(JobContext ctx)
+ public override Int32 Execute(JobContext ctx)
{
var msgs = ctx.Data as String[];
var sqls = Job.Data;
@@ -45,7 +45,7 @@ public class SqlMessage : MessageHandler
{
if (dt == null || dt.Columns == null || dt.Columns.Length == 0 || dt.Rows == null || dt.Rows.Count == 0) return;
- // select id as topic_roleId, id as topic_myId from role where updatetime>='{Start}' and updatetime<'{End}'
+ // select id as topic_roleId, id as topic_myId from role where updatetime>='{dt}' and updatetime<'{End}'
for (var i = 0; i < dt.Columns.Length; i++)
{
diff --git a/AntJob.Extensions/SqlSection.cs b/AntJob.Extensions/SqlSection.cs
index 04778a3..ae4194a 100644
--- a/AntJob.Extensions/SqlSection.cs
+++ b/AntJob.Extensions/SqlSection.cs
@@ -32,6 +32,12 @@ public class SqlSection
public String Sql { get; set; }
#endregion
+ #region 构造
+ /// <summary>已重载</summary>
+ /// <returns></returns>
+ public override String ToString() => $"{ConnName}[{Action}]:{Sql}";
+ #endregion
+
#region 解析
/// <summary>分析sql语句集合,得到片段集合,以双换行分隔</summary>
/// <param name="sqls"></param>
@@ -103,9 +109,25 @@ public class SqlSection
// 解析数据表,如果目标表不存在,则返回
var tableName = "";
if (Sql.StartsWithIgnoreCase("delete "))
- tableName = Sql.Substring(" from ", " ")?.Trim();
- else if (Sql.StartsWithIgnoreCase("udpate "))
- tableName = Sql.Substring("udpate ", " ")?.Trim();
+ {
+ var sep = " from ";
+ var p1 = Sql.IndexOf(sep);
+ if (p1 < 0) throw new InvalidDataException();
+
+ p1 += sep.Length;
+ var p2 = Sql.IndexOf(" ", p1);
+ tableName = p2 > 0 ? Sql[p1..p2].Trim() : Sql[p1..].Trim();
+ }
+ else if (Sql.StartsWithIgnoreCase("update "))
+ {
+ var sep = "update ";
+ var p1 = Sql.IndexOf(sep);
+ if (p1 < 0) throw new InvalidDataException();
+
+ p1 += sep.Length;
+ var p2 = Sql.IndexOf(" ", p1);
+ tableName = p2 > 0 ? Sql[p1..p2].Trim() : Sql[p1..].Trim();
+ }
if (!tableName.IsNullOrEmpty())
{
diff --git a/AntJob.Server/AntJob.Server.csproj b/AntJob.Server/AntJob.Server.csproj
index 5eb23f6..a8b1272 100644
--- a/AntJob.Server/AntJob.Server.csproj
+++ b/AntJob.Server/AntJob.Server.csproj
@@ -29,7 +29,7 @@
<Content Include="favicon.ico" />
</ItemGroup>
<ItemGroup>
- <PackageReference Include="NewLife.Redis" Version="5.6.2024.402" />
+ <PackageReference Include="NewLife.Redis" Version="6.0.2024.1006" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AntJob.Data\AntJob.Data.csproj" />
diff --git a/AntJob.Server/AntService.cs b/AntJob.Server/AntService.cs
index 5db006a..eaf3c37 100644
--- a/AntJob.Server/AntService.cs
+++ b/AntJob.Server/AntService.cs
@@ -5,18 +5,17 @@ using AntJob.Models;
using AntJob.Server.Services;
using NewLife;
using NewLife.Caching;
-using NewLife.Data;
using NewLife.Log;
using NewLife.Net;
using NewLife.Remoting;
+using NewLife.Remoting.Models;
using NewLife.Threading;
-using XCode;
namespace AntJob.Server;
/// <summary>蚂蚁服务层,Rpc接口服务</summary>
/// <remarks>
-/// 该服务层主要用于蚂蚁调度器与蚂蚁工作器之间的通讯,以及蚂蚁工作器与蚂蚁数据中心之间的通讯。
+/// 该服务层主要用于蚂蚁调度器与蚂蚁处理器之间的通讯,以及蚂蚁处理器与蚂蚁数据中心之间的通讯。
/// 服务注册到内部对象容器IObjectContainer,要求宿主ApiServer指定ServiceProvider为IObjectContainer。
/// </remarks>
[Api(null)]
@@ -65,7 +64,7 @@ class AntService : IApi, IActionFilter
}
else
{
- throw new ApiException(401, $"{_Net.Remote}未登录!不能执行{act}");
+ throw new ApiException(ApiCode.Unauthorized, $"{_Net.Remote}未登录!不能执行{act}");
}
}
@@ -90,23 +89,46 @@ class AntService : IApi, IActionFilter
}
#endregion
- #region 登录
+ #region 登录心跳
/// <summary>应用登录</summary>
/// <param name="model">模型</param>
/// <returns></returns>
[Api(nameof(Login))]
public LoginResponse Login(LoginModel model)
{
- if (model.User.IsNullOrEmpty()) throw new ArgumentNullException(nameof(model.User));
+ // 兼容旧版本
+ if (model.Code.IsNullOrEmpty() && !model.User.IsNullOrEmpty()) model.Code = model.User;
+ if (model.Secret.IsNullOrEmpty() && !model.Pass.IsNullOrEmpty()) model.Secret = model.Pass;
- var (app, rs) = _appService.Login(model, _Net.Remote.Host);
+ if (model.Code.IsNullOrEmpty()) throw new ArgumentNullException(nameof(model.Code));
+
+ var (app, online, rs) = _appService.Login(model, _Net.Remote.Host);
// 记录当前用户
Session["App"] = app;
+ Session["AppOnline"] = online;
return rs;
}
+ [Api(nameof(Logout))]
+ public ILogoutResponse Logout(String reason)
+ {
+ var app = Session["App"] as App;
+ var online = Session["AppOnline"] as AppOnline;
+
+ return _appService.Logout(app, online, reason, _Net.Remote.Host);
+ }
+
+ [Api(nameof(Ping))]
+ public IPingResponse Ping(PingRequest request)
+ {
+ var app = Session["App"] as App;
+ var online = Session["AppOnline"] as AppOnline;
+
+ return _appService.Ping(app, online, request, _Net.Remote.Host);
+ }
+
/// <summary>获取当前应用的所有在线实例</summary>
/// <returns></returns>
[Api(nameof(GetPeers))]
@@ -125,22 +147,33 @@ class AntService : IApi, IActionFilter
[Api(nameof(AddJobs))]
public String[] AddJobs(JobModel[] jobs)
{
- if (jobs == null || jobs.Length == 0) return new String[0];
+ if (jobs == null || jobs.Length == 0) return [];
return _jobService.AddJobs(_App, jobs);
}
+ /// <summary>设置作业。支持控制作业启停、数据时间、步进等参数</summary>
+ /// <returns></returns>
+ [Api(nameof(SetJob))]
+ public IJob SetJob(JobModel job) => _jobService.SetJob(_App, job, ControllerContext.Current.Parameters);
+
/// <summary>申请作业任务</summary>
/// <param name="model">模型</param>
/// <returns></returns>
[Api(nameof(Acquire))]
public ITask[] Acquire(AcquireModel model)
{
+ var span = DefaultSpan.Current;
+ if (span != null) span.Value = 0;
+
var job = model.Job?.Trim();
- if (job.IsNullOrEmpty()) return new TaskModel[0];
+ if (job.IsNullOrEmpty()) return [];
var ip = _Net.Remote.Host;
- return _jobService.Acquire(_App, model, ip);
+ var tasks = _jobService.Acquire(_App, model, ip);
+ if (span != null) span.Value = tasks?.Length ?? 0;
+
+ return tasks;
}
/// <summary>生产消息</summary>
diff --git a/AntJob.Server/Program.cs b/AntJob.Server/Program.cs
index 5ab5ada..5b4f408 100644
--- a/AntJob.Server/Program.cs
+++ b/AntJob.Server/Program.cs
@@ -4,6 +4,8 @@ using NewLife.Caching;
using NewLife.Caching.Services;
using NewLife.Log;
using NewLife.Model;
+using NewLife.Security;
+using NewLife.Serialization;
using Stardust;
using XCode;
@@ -28,6 +30,9 @@ if (set2.IsNew)
set2.Save();
}
+//// 过渡期暂时使用FastJson,为了兼容旧数据序列化Start
+//JsonHelper.Default = new FastJson();
+
services.AddSingleton(AntJobSetting.Current);
// 分布式缓存,锚定配置中心RedisCache,若无配置则使用本地MemoryCache
@@ -36,9 +41,15 @@ services.AddSingleton<ICacheProvider, RedisCacheProvider>();
services.AddSingleton<AppService>();
services.AddSingleton<JobService>();
+// 注册密码提供者,用于通信过程中保护密钥,避免明文传输
+services.AddSingleton<IPasswordProvider>(new SaltPasswordProvider { Algorithm = "md5", SaltTime = 60 });
+
// 预热数据层,执行反向工程建表等操作
EntityFactory.InitConnection("Ant");
+// 修正旧版数据
+_ = Task.Run(() => JobService.FixOld());
+
// 友好退出
var host = services.BuildHost();
diff --git a/AntJob.Server/Services/AppService.cs b/AntJob.Server/Services/AppService.cs
index 250636c..6dcb19b 100644
--- a/AntJob.Server/Services/AppService.cs
+++ b/AntJob.Server/Services/AppService.cs
@@ -3,9 +3,9 @@ using AntJob.Data;
using AntJob.Data.Entity;
using AntJob.Models;
using NewLife;
-using NewLife.Caching;
using NewLife.Log;
using NewLife.Remoting;
+using NewLife.Remoting.Models;
using NewLife.Security;
using NewLife.Web;
@@ -13,14 +13,14 @@ namespace AntJob.Server.Services;
public class AppService
{
- private readonly ICacheProvider _cacheProvider;
- private readonly ITracer _tracer;
+ private readonly IPasswordProvider _passwordProvider;
+ private readonly AntJobSetting _setting;
private readonly ILog _log;
- public AppService(ICacheProvider cacheProvider, ITracer tracer, ILog log)
+ public AppService(IPasswordProvider passwordProvider, AntJobSetting setting, ILog log)
{
- _cacheProvider = cacheProvider;
- _tracer = tracer;
+ _passwordProvider = passwordProvider;
+ _setting = setting;
_log = log;
}
@@ -28,52 +28,57 @@ public class AppService
/// <summary>应用登录</summary>
/// <param name="model">模型</param>
/// <returns></returns>
- public (App, LoginResponse) Login(LoginModel model, String ip)
+ public (App, AppOnline, LoginResponse) Login(LoginModel model, String ip)
{
- if (model.User.IsNullOrEmpty()) throw new ArgumentNullException(nameof(model.User));
+ if (model.Code.IsNullOrEmpty()) throw new ArgumentNullException(nameof(model.Code));
- _log.Info("[{0}]从[{1}]登录[{2}@{3}]", model.User, ip, model.Machine, model.ProcessId);
+ _log.Info("[{0}]从[{1}]登录[{2}]", model.Code, ip, model.ClientId);
// 找应用
var autoReg = false;
- var app = App.FindByName(model.User);
- if (app == null || app.Secret.MD5() != model.Pass)
+ var app = App.FindByName(model.Code);
+
+ // 登录密码未设置或者未提交,则执行动态注册
+ var secret = model.Secret;
+ if (app == null || !app.Secret.IsNullOrEmpty() && secret.IsNullOrEmpty())
{
- app = CheckApp(app, model.User, model.Pass, ip);
- if (app == null) throw new ArgumentOutOfRangeException(nameof(model.User));
+ app = CheckApp(app, model.Code, model.Secret, ip);
+ if (app == null) throw new ArgumentOutOfRangeException(nameof(model.Code));
autoReg = true;
}
- if (app == null) throw new Exception($"应用[{model.User}]不存在!");
+ if (app == null) throw new Exception($"应用[{model.Code}]不存在!");
if (!app.Enable) throw new Exception("已禁用!");
// 核对密码
if (!autoReg && !app.Secret.IsNullOrEmpty())
{
- var pass2 = app.Secret.MD5();
- if (model.Pass != pass2) throw new Exception("密码错误!");
+ if (secret.IsNullOrEmpty() || !_passwordProvider.Verify(app.Secret, secret)) throw new Exception("密码错误!");
}
// 版本和编译时间
if (app.Version.IsNullOrEmpty() || app.Version.CompareTo(model.Version) < 0) app.Version = model.Version;
- if (app.CompileTime < model.Compile) app.CompileTime = model.Compile;
+ var compile = model.Compile.ToDateTime().ToLocalTime();
+ if (app.CompileTime < compile) app.CompileTime = compile;
if (app.DisplayName.IsNullOrEmpty()) app.DisplayName = model.DisplayName;
app.Save();
// 应用上线
- var online = CreateOnline(app, ip, model.Machine, model.ProcessId);
+ var online = CreateOnline(app, ip, model.ClientId);
+ online.Name = model.Machine;
+ online.ProcessId = model.ProcessId;
online.Version = model.Version;
- online.CompileTime = model.Compile;
+ online.CompileTime = compile;
online.Save();
- WriteHistory(app, autoReg ? "注册" : "登录", true, $"[{model.User}/{model.Pass}]在[{model.Machine}@{model.ProcessId}]登录[{app}]成功");
+ WriteHistory(app, autoReg ? "注册" : "登录", true, $"[{model.Code}/{model.Secret}]在[{model.ClientId}]登录[{app}]成功", ip);
- var rs = new LoginResponse { Name = app.Name, DisplayName = app.DisplayName };
+ var rs = new LoginResponse { Name = app.Name };
if (autoReg) rs.Secret = app.Secret;
- return (app, rs);
+ return (app, online, rs);
}
protected virtual App CheckApp(App app, String user, String pass, String ip)
@@ -83,21 +88,21 @@ public class AppService
if (app == null)
{
// 是否支持自动注册
- var set = AntJobSetting.Current;
- if (!set.AutoRegistry) throw new Exception($"找不到应用[{name}]");
+ //var set = AntJobSetting.Current;
+ if (!_setting.AutoRegistry) throw new Exception($"找不到应用[{name}]");
app = new App
{
- Secret = Rand.NextString(16)
+ //Secret = Rand.NextString(16)
};
}
else if (app.Secret.MD5() != pass)
{
// 是否支持自动注册
- var set = AntJobSetting.Current;
- if (!set.AutoRegistry) throw new Exception($"应用[{name}]申请重新激活,但服务器设置禁止自动注册");
+ //var set = AntJobSetting.Current;
+ if (!_setting.AutoRegistry) throw new Exception($"应用[{name}]申请重新激活,但服务器设置禁止自动注册");
- if (app.Secret.IsNullOrEmpty()) app.Secret = Rand.NextString(16);
+ //if (app.Secret.IsNullOrEmpty()) app.Secret = Rand.NextString(16);
}
if (app.ID == 0)
@@ -117,9 +122,48 @@ public class AppService
return app;
}
+
+ public ILogoutResponse Logout(App app, AppOnline online, String reason, String ip)
+ {
+ if (app != null)
+ {
+ online ??= GetOnline(app, ip);
+ if (online != null)
+ {
+ WriteHistory(app, "注销", true, reason, ip);
+
+ online.Delete();
+ }
+ }
+
+ return new LogoutResponse { Name = app?.Name };
+ }
#endregion
#region 在线状态
+ public IPingResponse Ping(App app, AppOnline online, IPingRequest request, String ip)
+ {
+ if (app != null)
+ {
+ online ??= GetOnline(app, ip);
+ if (online != null)
+ {
+ if (request is PingRequest req)
+ {
+ }
+ online.UpdateIP = ip;
+
+ online.Save();
+ }
+ }
+
+ return new PingResponse
+ {
+ Time = request.Time,
+ ServerTime = DateTime.UtcNow.ToLong(),
+ };
+ }
+
/// <summary>获取当前应用的所有在线实例</summary>
/// <returns></returns>
public PeerModel[] GetPeers(App app)
@@ -129,17 +173,13 @@ public class AppService
return olts.Select(e => e.ToModel()).ToArray();
}
- AppOnline CreateOnline(App app, String ip, String machine, Int32 pid)
+ AppOnline CreateOnline(App app, String ip, String clientId)
{
var online = GetOnline(app, ip);
- online.Client = $"{(ip.IsNullOrEmpty() ? machine : ip)}@{pid}";
- online.Name = machine;
- online.ProcessId = pid;
+ online.Client = clientId;
online.UpdateIP = ip;
- //online.Version = version;
online.Server = Environment.MachineName;
- //online.Save();
return online;
}
@@ -147,7 +187,7 @@ public class AppService
public AppOnline GetOnline(App app, String ip)
{
var ins = $"{app.Name}@{ip}";
- var online = AppOnline.FindByInstance(ins) ?? new AppOnline { CreateIP = ip };
+ var online = AppOnline.FindByInstance(ins) ?? new AppOnline { Enable = true, CreateIP = ip };
online.AppID = app.ID;
online.Instance = ins;
diff --git a/AntJob.Server/Services/JobService.cs b/AntJob.Server/Services/JobService.cs
index 7c87641..99b2c0a 100644
--- a/AntJob.Server/Services/JobService.cs
+++ b/AntJob.Server/Services/JobService.cs
@@ -3,28 +3,21 @@ using AntJob.Data.Entity;
using AntJob.Models;
using NewLife;
using NewLife.Caching;
-using NewLife.Data;
using NewLife.Log;
+using NewLife.Reflection;
using NewLife.Serialization;
using NewLife.Threading;
using XCode;
+using XCode.DataAccessLayer;
namespace AntJob.Server.Services;
-public class JobService
+public class JobService(AppService appService, ICacheProvider cacheProvider, ITracer tracer, ILog log)
{
- private readonly AppService _appService;
- private readonly ICacheProvider _cacheProvider;
- private readonly ITracer _tracer;
- private readonly ILog _log;
-
- public JobService(AppService appService, ICacheProvider cacheProvider, ITracer tracer, ILog log)
- {
- _appService = appService;
- _cacheProvider = cacheProvider;
- _tracer = tracer;
- _log = log;
- }
+ private readonly AppService _appService = appService;
+ private readonly ICacheProvider _cacheProvider = cacheProvider;
+ private readonly ITracer _tracer = tracer;
+ private readonly ILog _log = log;
#region 业务
/// <summary>获取指定名称的作业</summary>
@@ -33,7 +26,26 @@ public class JobService
{
var jobs = Job.FindAllByAppID(app.ID);
- return jobs.Select(e => e.ToModel()).ToArray();
+ //return jobs.Select(e => e.ToModel()).ToArray();
+
+ // 服务端下发的时间,约定UTC
+ var rs = new List<IJob>();
+ foreach (var job in jobs)
+ {
+ var model = job.ToModel();
+#pragma warning disable CS0612 // 类型或成员已过时
+ // 为兼容旧版
+ model.Start = model.DataTime;
+#pragma warning restore CS0612 // 类型或成员已过时
+
+ model.DataTime = model.DataTime.ToUniversalTime();
+ if (model.End.Year > 1000)
+ model.End = model.End.ToUniversalTime();
+
+ rs.Add(model);
+ }
+
+ return rs.ToArray();
}
/// <summary>批量添加作业</summary>
@@ -41,79 +53,114 @@ public class JobService
/// <returns></returns>
public String[] AddJobs(App app, JobModel[] jobs)
{
- if (jobs == null || jobs.Length == 0) return new String[0];
+ if (jobs == null || jobs.Length == 0) return [];
var myJobs = Job.FindAllByAppID(app.ID);
var list = new List<String>();
- foreach (var item in jobs)
+ foreach (var model in jobs)
{
- var jb = myJobs.FirstOrDefault(e => e.Name.EqualIgnoreCase(item.Name));
- jb ??= new Job
+ // 客户端上报的时间,约定UTC,需要转为本地时间
+ var job = myJobs.FirstOrDefault(e => e.Name.EqualIgnoreCase(model.Name));
+ job ??= new Job
{
AppID = app.ID,
- Name = item.Name,
- Enable = item.Enable,
- Start = item.Start,
- End = item.End,
- Offset = item.Offset,
- Step = item.Step,
- BatchSize = item.BatchSize,
- MaxTask = item.MaxTask,
- Mode = item.Mode,
+ Name = model.Name,
+ Enable = model.Enable,
+ DataTime = model.DataTime.ToLocalTime(),
+ End = model.End.Year > 1000 ? model.End.ToLocalTime() : model.End,
+ Offset = model.Offset,
+ Step = model.Step,
+ BatchSize = model.BatchSize,
+ MaxTask = model.MaxTask,
+ Mode = model.Mode,
MaxError = 100,
};
- if (item.Mode > 0) jb.Mode = item.Mode;
- if (!item.DisplayName.IsNullOrEmpty()) jb.DisplayName = item.DisplayName;
- if (!item.Description.IsNullOrEmpty()) jb.Remark = item.Description;
- if (!item.ClassName.IsNullOrEmpty()) jb.ClassName = item.ClassName;
- if (jb.Topic.IsNullOrEmpty()) jb.Topic = item.Topic;
+ if (model.Mode > 0) job.Mode = model.Mode;
+ if (!model.DisplayName.IsNullOrEmpty()) job.DisplayName = model.DisplayName;
+ if (!model.Description.IsNullOrEmpty()) job.Remark = model.Description;
+ if (!model.ClassName.IsNullOrEmpty()) job.ClassName = model.ClassName;
+ if (job.Cron.IsNullOrEmpty()) job.Cron = model.Cron;
+ if (job.Topic.IsNullOrEmpty()) job.Topic = model.Topic;
- if (jb.Save() != 0)
+ // 添加定时作业时,计算下一次执行时间
+ if (job.ID == 0 && job.Mode == JobModes.Time)
+ {
+ var next = job.GetNext();
+ if (next.Year > 2000) job.DataTime = next;
+ }
+
+ if (job.Save() != 0)
{
// 更新作业数
- jb.SaveAsync();
+ job.SaveAsync();
- list.Add(jb.Name);
+ list.Add(job.Name);
}
}
return list.ToArray();
}
+ /// <summary>设置作业。支持控制作业启停、数据时间、步进等参数</summary>
+ /// <param name="app"></param>
+ /// <param name="model"></param>
+ /// <returns></returns>
+ public IJob SetJob(App app, JobModel model, IDictionary<String, Object> parameters)
+ {
+ var job = Job.FindByAppIDAndName(app.ID, model.Name);
+ if (job == null) return null;
+
+ // 可以修改的字段
+ var fs = new[] { nameof(IJob.Enable), nameof(IJob.DataTime), nameof(IJob.End), nameof(IJob.Step), nameof(JobModel.DisplayName), nameof(JobModel.Description), nameof(IJob.Topic), nameof(IJob.Data) };
+ foreach (var item in fs)
+ {
+ if (parameters.ContainsKey(item))
+ job.SetItem(item, model.GetValue(item));
+ }
+
+ job.Update();
+
+ return job.ToModel();
+ }
+
/// <summary>申请作业任务</summary>
/// <param name="model">模型</param>
/// <returns></returns>
public ITask[] Acquire(App app, AcquireModel model, String ip)
{
- var job = model.Job?.Trim();
- if (job.IsNullOrEmpty()) return new TaskModel[0];
+ var jobName = model.Job?.Trim();
+ if (jobName.IsNullOrEmpty()) return [];
- if (app == null) return new TaskModel[0];
+ if (app == null) return [];
// 应用停止发放作业
app = App.FindByID(app.ID) ?? app;
- if (!app.Enable) return new TaskModel[0];
- var jb = app.Jobs.FirstOrDefault(e => e.Name == job);
+ if (!app.Enable) return [];
+
+ var job = app.Jobs.FirstOrDefault(e => e.Name == jobName);
// 全局锁,确保单个作业只有一个线程在分配作业
- using var ck = _cacheProvider.AcquireLock($"antjob:lock:{jb.ID}", 15_000);
+ using var ck = _cacheProvider.AcquireLock($"antjob:lock:{job.ID}", 15_000);
// 找到作业。为了确保能够快速拿到新的作业参数,这里做二次查询
- if (jb != null)
- jb = Job.Find(Job._.ID == jb.ID);
+ if (job != null)
+ job = Job.Find(Job._.ID == job.ID);
else
- jb = Job.FindByAppIDAndName(app.ID, job);
+ job = Job.FindByAppIDAndName(app.ID, jobName);
- if (jb == null) throw new XException($"应用[{app.ID}/{app.Name}]下未找到作业[{job}]");
- if (jb.Step == 0 || jb.Start.Year <= 2000) throw new XException("作业[{0}/{1}]未设置开始时间或步进", jb.ID, jb.Name);
+ if (job == null) throw new XException($"应用[{app.ID}/{app.Name}]下未找到作业[{jobName}]");
+ if (job.DataTime.Year <= 2000) throw new XException("作业[{0}/{1}]未设置数据时间", job.ID, job.Name);
+ // 应用在线,但可能禁止向其分配任务
var online = _appService.GetOnline(app, ip);
+ if (!online.Enable) return [];
var list = new List<JobTask>();
- // 每分钟检查一下错误任务和中断任务
- CheckErrorTask(app, jb, model.Count, list, ip);
+ // 首先检查延迟任务和错误任务
+ CheckDelayTask(app, job, model.Count, list, ip);
+ CheckOldTask(app, job, model.Count, list, ip);
// 错误项不够时,增加切片
if (list.Count < model.Count)
@@ -123,63 +170,146 @@ public class JobService
var pid = online.ProcessId;
//var topic = ps["topic"] + "";
- switch (jb.Mode)
+ switch (job.Mode)
{
case JobModes.Message:
- list.AddRange(jb.AcquireMessage(model.Topic, server, ip, pid, model.Count - list.Count, _cacheProvider.Cache));
+ list.AddRange(AcquireMessage(job, model.Topic, server, ip, pid, model.Count - list.Count, _cacheProvider.Cache));
break;
+ case JobModes.Time:
+ {
+ // 如果能够切片,则查询数据库后进入,避免缓存导致重复
+ if (TrySplitTime(job, out _))
+ {
+ // 申请任务前,不能再查数据库,那样子会导致多线程脏读,从而出现多客户端分到相同任务的情况
+ //jb = Job.FindByKey(jb.ID);
+ list.AddRange(Acquire(job, server, ip, pid, model.Count - list.Count, _cacheProvider.Cache));
+ }
+ break;
+ }
case JobModes.Data:
- case JobModes.Alarm:
//case JobModes.CSharp:
//case JobModes.Sql:
default:
{
// 如果能够切片,则查询数据库后进入,避免缓存导致重复
- if (jb.TrySplit(jb.Start, jb.Step, out var end))
+ if (TrySplit(job, job.DataTime, out var end))
{
// 申请任务前,不能再查数据库,那样子会导致多线程脏读,从而出现多客户端分到相同任务的情况
//jb = Job.FindByKey(jb.ID);
- list.AddRange(jb.Acquire(server, ip, pid, model.Count - list.Count, _cacheProvider.Cache));
+ list.AddRange(Acquire(job, server, ip, pid, model.Count - list.Count, _cacheProvider.Cache));
}
}
break;
}
}
+ if (list.Count > 0)
+ {
+ job.LastStatus = JobStatus.处理中;
+ job.LastTime = DateTime.Now;
+
+ job.UpdateTime = DateTime.Now;
+ job.Save();
+ }
+
// 记录状态
online.Tasks += list.Count;
online.SaveAsync();
- return list.Select(e => e.ToModel()).ToArray();
+ //return list.Select(e => e.ToModel()).ToArray();
+
+ // 服务端下发的时间,约定UTC
+ var rs = new List<ITask>();
+ foreach (var task in list)
+ {
+ var model2 = task.ToModel();
+#pragma warning disable CS0612 // 类型或成员已过时
+ // 为兼容旧版
+ model2.Start = model2.DataTime;
+#pragma warning restore CS0612 // 类型或成员已过时
+
+ model2.DataTime = model2.DataTime.ToUniversalTime();
+ if (model2.End.Year > 1000)
+ model2.End = model2.End.ToUniversalTime();
+
+ rs.Add(model2);
+ }
+
+ return rs.ToArray();
+ }
+
+ private void CheckDelayTask(App app, Job job, Int32 count, List<JobTask> list, String ip)
+ {
+ // 获取下一次检查时间
+ var cache = _cacheProvider.Cache;
+ var nextKey = $"antjob:NextDelay_{job.ID}";
+ var now = TimerX.Now;
+ var next = cache.Get<DateTime>(nextKey);
+ if (next >= now)
+ {
+ // 如果常规检查时间未到,看看是否有挂起任务
+ var pendingKey = $"antjob:PendingDelay_{job.ID}";
+ var pending = cache.Get<DateTime>(pendingKey);
+ if (pending.Year > 2000) next = pending;
+ }
+ if (next <= now)
+ {
+ var online = _appService.GetOnline(app, ip);
+
+ next = now.AddSeconds(15);
+ list.AddRange(AcquireDelay(job, online.Server, ip, online.ProcessId, count, cache));
+
+ if (list.Count > 0)
+ {
+ // 既然有数据,待会还来
+ next = now;
+
+ _log.Info("作业[{0}/{1}]准备处理[{2}]个延迟任务 [{3}]", app, job.Name, list.Count, list.Join(",", e => e.ID + ""));
+ }
+
+ cache.Set(nextKey, next);
+ }
}
- private void CheckErrorTask(App app, Job jb, Int32 count, List<JobTask> list, String ip)
+ private void CheckOldTask(App app, Job job, Int32 count, List<JobTask> list, String ip)
{
// 每分钟检查一下错误任务和中断任务
- var nextKey = $"antjob:NextAcquireOld_{jb.ID}";
+ var cache = _cacheProvider.Cache;
+ var nextKey = $"antjob:NextOld_{job.ID}";
var now = TimerX.Now;
- var next = _cacheProvider.Cache.Get<DateTime>(nextKey);
+ var next = cache.Get<DateTime>(nextKey);
if (next < now)
{
var online = _appService.GetOnline(app, ip);
next = now.AddSeconds(60);
- list.AddRange(jb.AcquireOld(online.Server, ip, online.ProcessId, count, _cacheProvider.Cache));
+ list.AddRange(AcquireOld(job, online.Server, ip, online.ProcessId, count, cache));
if (list.Count > 0)
{
// 既然有数据,待会还来
next = now;
- var n1 = list.Count(e => e.Status == JobStatus.错误 || e.Status == JobStatus.取消);
- var n2 = list.Count(e => e.Status == JobStatus.就绪 || e.Status == JobStatus.抽取中 || e.Status == JobStatus.处理中);
- _log.Info("作业[{0}/{1}]准备处理[{2}]个错误和[{3}]超时任务 [{4}]", app, jb.Name, n1, n2, list.Join(",", e => e.ID + ""));
+ var n1 = list.Count(e => e.Status is JobStatus.错误);
+ var n2 = list.Count(e => e.Status is JobStatus.就绪 or JobStatus.抽取中 or JobStatus.处理中);
+ _log.Info("作业[{0}/{1}]准备处理[{2}]个错误和[{3}]超时任务 [{4}]", app, job.Name, n1, n2, list.Join(",", e => e.ID + ""));
}
- else
- _cacheProvider.Cache.Set(nextKey, next);
+
+ cache.Set(nextKey, next);
}
}
+ /// <summary>设置作业最近一个延迟任务的时间</summary>
+ /// <param name="jobId"></param>
+ /// <param name="nextTime"></param>
+ public void SetDelay(Int32 jobId, DateTime nextTime)
+ {
+ var nextKey = $"antjob:PendingDelay_{jobId}";
+ var next = _cacheProvider.Cache.Get<DateTime>(nextKey);
+ if (next.Year < 2000 || next > nextTime)
+ _cacheProvider.Cache.Set(nextKey, nextTime, 600);
+ }
+
/// <summary>生产消息</summary>
/// <param name="model">模型</param>
/// <returns></returns>
@@ -231,12 +361,10 @@ public class JobService
jm.CreateTime = jm.UpdateTime = now;
- // 雪花Id直接指定消息在未来的消费时间
if (model.DelayTime > 0)
- {
- jm.Id = snow.NewId(dTime);
- jm.UpdateTime = dTime;
- }
+ jm.DelayTime = dTime;
+ else
+ jm.DelayTime = now;
ms.Add(jm);
}
@@ -265,69 +393,98 @@ public class JobService
#region 状态报告
/// <summary>报告状态(进度、成功、错误)</summary>
- /// <param name="task"></param>
+ /// <param name="result"></param>
/// <returns></returns>
- public Boolean Report(App app, TaskResult task, String ip)
+ public Boolean Report(App app, TaskResult result, String ip)
{
- if (task == null || task.ID == 0) throw new InvalidOperationException("无效操作 TaskID=" + task?.ID);
+ if (result == null || result.ID == 0) throw new InvalidOperationException("无效操作 TaskID=" + result?.ID);
// 判断是否有权
- var jt = JobTask.FindByID(task.ID) ?? throw new InvalidOperationException($"找不到任务[{task.ID}]");
- var job = Job.FindByID(jt.JobID);
+ var task = JobTask.FindByID(result.ID) ?? throw new InvalidOperationException($"找不到任务[{result.ID}]");
+ var job = Job.FindByID(task.JobID);
if (job == null || job.AppID != app.ID)
{
- _log.Info(task.ToJson());
- throw new InvalidOperationException($"应用[{app}]无权操作作业[{job}#{jt}]");
+ _log.Info(result.ToJson());
+ throw new InvalidOperationException($"应用[{app}]无权操作作业[{job}#{task}]");
}
// 只有部分字段允许客户端修改
- if (task.Status > 0) jt.Status = task.Status;
-
- jt.Speed = task.Speed;
- jt.Total = task.Total;
- jt.Success = task.Success;
- jt.Cost = task.Cost;
- jt.Key = task.Key;
- jt.Message = task.Message;
-
- // 已终结的作业,汇总统计
- if (task.Status == JobStatus.完成 || task.Status == JobStatus.错误)
+ if (result.Status > 0) task.Status = result.Status;
+
+ task.Speed = result.Speed;
+ task.Total = result.Total;
+ task.Success = result.Success;
+ task.Cost = (Int32)Math.Round(result.Cost / 1000d);
+ task.Key = result.Key;
+ task.Message = result.Message;
+
+ var traceId = result.TraceId ?? DefaultSpan.Current + "";
+ // 已终结的任务,汇总统计
+ if (result.Status is JobStatus.完成)
{
- jt.Times++;
+ task.Times++;
- SetJobFinish(job, jt);
+ SetJobFinish(job, task);
// 记录状态
- _appService.UpdateOnline(app, jt, ip);
+ _appService.UpdateOnline(app, task, ip);
}
- if (task.Status == JobStatus.错误)
+ else if (result.Status == JobStatus.错误)
{
- SetJobError(job, jt);
-
- jt.Error++;
+ task.Times++;
+ task.Error++;
//ji.Message = err.Message;
+ SetJobError(job, task, ip);
+
// 出错时判断如果超过最大错误数,则停止作业
CheckMaxError(app, job);
+
+ // 记录状态
+ _appService.UpdateOnline(app, task, ip);
+ }
+ else if (result.Status == JobStatus.延迟)
+ {
+ using var span = _tracer?.NewSpan("Delay", new { job.Name, task.DataTime, NextTime = result.NextTime.ToLocalTime() });
+
+ task.Times++;
+
+ // 延迟任务的下一次执行时间
+ if (result.NextTime.Year > 2000)
+ {
+ task.UpdateTime = result.NextTime.ToLocalTime();
+
+ SetDelay(task.JobID, task.UpdateTime);
+ }
+ else
+ {
+ SetDelay(task.JobID, DateTime.Now.AddSeconds(job.ErrorDelay));
+ }
}
// 从创建到完成的全部耗时
- var ts = DateTime.Now - jt.CreateTime;
- jt.FullCost = (Int32)ts.TotalSeconds;
+ var ts = DateTime.Now - task.CreateTime;
+ task.FullCost = (Int32)ts.TotalSeconds;
- jt.SaveAsync();
- //ji.Save();
+ task.Update();
+
+ job.LastStatus = result.Status;
+ job.LastTime = DateTime.Now;
+ job.SaveAsync();
return true;
}
private void SetJobFinish(Job job, JobTask task)
{
+ using var span = _tracer?.NewSpan(nameof(SetJobFinish), new { job.Name, task.DataTime });
+
job.Total += task.Total;
job.Success += task.Success;
- job.Error += task.Error;
+ //job.Error += task.Error;
job.Times++;
+ if (task.Status == JobStatus.错误) job.Error++;
var ths = job.MaxTask;
@@ -337,7 +494,7 @@ public class JobService
{
// 平均速度
if (job.Speed > 0)
- job.Speed = (Int32)((job.Speed * 3L + p1) / 4);
+ job.Speed = (Int32)Math.Round((job.Speed * 3d + p1) / 4);
else
job.Speed = p1;
}
@@ -346,14 +503,16 @@ public class JobService
//job.Save();
}
- private JobError SetJobError(Job job, JobTask task)
+ private JobError SetJobError(Job job, JobTask task, String ip)
{
+ using var span = _tracer?.NewSpan(nameof(SetJobError), new { job.Name, task.DataTime });
+
var err = new JobError
{
AppID = job.AppID,
JobID = job.ID,
TaskID = task.ID,
- Start = task.Start,
+ DataTime = task.DataTime,
End = task.End,
Data = task.Data,
@@ -361,6 +520,8 @@ public class JobService
ProcessID = task.ProcessID,
Client = task.Client,
+ CreateIP = ip,
+ UpdateIP = ip,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
};
@@ -377,7 +538,7 @@ public class JobService
private void CheckMaxError(App app, Job job)
{
// 出错时判断如果超过最大错误数,则停止作业
- var maxError = job.MaxError < 1 ? 100 : job.MaxError;
+ var maxError = job.MaxError <= 0 ? 100 : job.MaxError;
if (job.Enable && job.Error > maxError)
{
job.MaxError = maxError;
@@ -388,4 +549,369 @@ public class JobService
}
}
#endregion
+
+ #region 申请任务
+ /// <summary>用于表示切片批次的序号</summary>
+ private Int32 _idxBatch;
+
+ /// <summary>申请任务分片(时间调度&数据调度)</summary>
+ /// <param name="server">申请任务的服务端</param>
+ /// <param name="ip">申请任务的IP</param>
+ /// <param name="pid">申请任务的服务端进程ID</param>
+ /// <param name="count">要申请的任务个数</param>
+ /// <param name="cache">缓存对象</param>
+ /// <returns></returns>
+ public IList<JobTask> Acquire(Job job, String server, String ip, Int32 pid, Int32 count, ICache cache)
+ {
+ var list = new List<JobTask>();
+
+ if (!job.Enable) return list;
+
+ using var span = _tracer?.NewSpan(nameof(Acquire), new { job.Name, server, ip, pid, count });
+
+ using var ts = Job.Meta.CreateTrans();
+ var start = job.DataTime;
+ for (var i = 0; i < count; i++)
+ {
+ var end = DateTime.MinValue;
+ if (job.Mode == JobModes.Time && !TrySplitTime(job, out end) ||
+ job.Mode != JobModes.Time && !TrySplit(job, start, out end))
+ break;
+ if (end.Year < 2000 || end.Year > 9000)
+ throw new ArgumentOutOfRangeException(nameof(end), end, "结束时间不合法");
+
+ // 创建新的任务
+ var task = new JobTask
+ {
+ AppID = job.AppID,
+ JobID = job.ID,
+ DataTime = start,
+ End = end,
+ Data = job.Data,
+ BatchSize = job.BatchSize,
+
+ Server = server,
+ ProcessID = Interlocked.Increment(ref _idxBatch),
+ Client = $"{ip}@{pid}",
+ Status = JobStatus.就绪,
+ CreateTime = DateTime.Now,
+ UpdateTime = DateTime.Now
+ };
+
+ //// 如果有模板,则进行计算替换
+ //if (!Data.IsNullOrEmpty()) ti.Data = TemplateHelper.Build(Data, ti.DataTime, ti.End);
+
+ // 重复切片判断
+ var key = $"job:task:{job.ID}:{start:yyyyMMddHHmmss}";
+ if (!cache.Add(key, task, 30))
+ {
+ var ti2 = cache.Get<JobTask>(key);
+ XTrace.WriteLine("[{0}]重复切片:{1}", key, ti2?.ToJson());
+ using var span2 = DefaultTracer.Instance?.NewSpan($"job:AcquireDuplicate", ti2);
+ }
+ else
+ {
+ task.Insert();
+
+ list.Add(task);
+ }
+
+ // 更新任务
+ job.DataTime = end;
+ start = end;
+ }
+
+ ts.Commit();
+
+ // 记录任务数
+ span?.AppendTag(null, list.Count);
+
+ return list;
+ }
+
+ /// <summary>尝试分割时间片(时间调度)</summary>
+ /// <param name="job"></param>
+ /// <param name="end"></param>
+ /// <returns></returns>
+ public Boolean TrySplitTime(Job job, out DateTime end)
+ {
+ var start = job.DataTime;
+
+ // 当前时间减去偏移量,作为当前时间。数据抽取不许超过该时间。去掉毫秒
+ var now = DateTime.Now.AddSeconds(-job.Offset).Trim("s");
+
+ end = DateTime.MinValue;
+
+ // 开始时间和结束时间是否越界
+ if (start >= now) return false;
+
+ // 计算下一次执行时间
+ end = job.GetNext();
+
+ // 任务结束时间超过作业结束时间时,取后者
+ if (job.End.Year > 2000 && end > job.End) end = job.End;
+
+ // 时间区间判断
+ if (end.Year > 2000 && start >= end) return false;
+
+ return true;
+ }
+
+ /// <summary>尝试分割时间片(数据调度)</summary>
+ /// <param name="start"></param>
+ /// <param name="step"></param>
+ /// <param name="end"></param>
+ /// <returns></returns>
+ public Boolean TrySplit(Job job, DateTime start, out DateTime end)
+ {
+ // 当前时间减去偏移量,作为当前时间。数据抽取不许超过该时间
+ var now = DateTime.Now.AddSeconds(-job.Offset);
+ // 去掉毫秒
+ now = now.Trim();
+
+ end = DateTime.MinValue;
+
+ // 开始时间和结束时间是否越界
+ if (start >= now) return false;
+
+ var step = job.Step;
+ if (step <= 0) step = 30;
+
+ // 必须严格要求按照步进大小分片,除非有合适的End
+ end = start.AddSeconds(step);
+ // 任务结束时间超过作业结束时间时,取后者
+ if (job.End.Year > 2000 && end > job.End) end = job.End;
+
+ // 时间片必须严格要求按照步进大小分片,除非有合适的End
+ if (end > now) return false;
+
+ // 时间区间判断
+ if (start >= end) return false;
+
+ return true;
+ }
+
+ /// <summary>申请延迟/取消任务</summary>
+ /// <param name="server">申请任务的服务端</param>
+ /// <param name="ip">申请任务的IP</param>
+ /// <param name="pid">申请任务的服务端进程ID</param>
+ /// <param name="count">要申请的任务个数</param>
+ /// <param name="cache">缓存对象</param>
+ /// <returns></returns>
+ public IList<JobTask> AcquireDelay(Job job, String server, String ip, Int32 pid, Int32 count, ICache cache)
+ {
+ using var span = _tracer?.NewSpan(nameof(AcquireDelay), new { job.Name, server, ip, pid, count });
+
+ using var ts = Job.Meta.CreateTrans();
+
+ var now = DateTime.Now;
+ var maxError = job.MaxError - job.Error;
+ var list = JobTask.Search(job.ID, now.AddDays(-7), now, job.MaxRetry, maxError, [JobStatus.取消, JobStatus.延迟], count);
+ foreach (var task in list)
+ {
+ task.Server = server;
+ task.ProcessID = Interlocked.Increment(ref _idxBatch);
+ task.Client = $"{ip}@{pid}";
+ task.Status = JobStatus.就绪;
+ //task.CreateTime = DateTime.Now;
+ task.UpdateTime = DateTime.Now;
+ }
+ list.Save();
+
+ ts.Commit();
+
+ // 记录任务数
+ span?.AppendTag(null, list.Count);
+
+ return list;
+ }
+
+ /// <summary>申请历史错误或中断的任务</summary>
+ /// <param name="server">申请任务的服务端</param>
+ /// <param name="ip">申请任务的IP</param>
+ /// <param name="pid">申请任务的服务端进程ID</param>
+ /// <param name="count">要申请的任务个数</param>
+ /// <param name="cache">缓存对象</param>
+ /// <returns></returns>
+ public IList<JobTask> AcquireOld(Job job, String server, String ip, Int32 pid, Int32 count, ICache cache)
+ {
+ using var span = _tracer?.NewSpan(nameof(AcquireOld), new { job.Name, server, ip, pid, count });
+
+ using var ts = Job.Meta.CreateTrans();
+ var list = new List<JobTask>();
+
+ var now = DateTime.Now;
+ var maxError = job.MaxError - job.Error;
+
+ // 查找历史错误任务
+ if (job.ErrorDelay > 0)
+ {
+ var end = now.AddSeconds(-job.ErrorDelay);
+ var list2 = JobTask.Search(job.ID, now.AddDays(-7), end, job.MaxRetry, maxError, [JobStatus.错误], count);
+ if (list2.Count > 0) list.AddRange(list2);
+ }
+
+ // 查找历史中断任务,持续10分钟仍然未完成
+ if (job.MaxTime > 0 && list.Count < count)
+ {
+ var end = now.AddSeconds(-job.MaxTime);
+ var list2 = JobTask.Search(job.ID, now.AddDays(-7), end, job.MaxRetry, maxError, [JobStatus.就绪, JobStatus.抽取中, JobStatus.处理中], count - list.Count);
+ if (list2.Count > 0) list.AddRange(list2);
+ }
+ if (list.Count > 0)
+ {
+ foreach (var task in list)
+ {
+ task.Server = server;
+ task.ProcessID = Interlocked.Increment(ref _idxBatch);
+ task.Client = $"{ip}@{pid}";
+ task.Status = JobStatus.就绪;
+ //task.CreateTime = DateTime.Now;
+ task.UpdateTime = DateTime.Now;
+ }
+ list.Save();
+ }
+
+ ts.Commit();
+
+ // 记录任务数
+ span?.AppendTag(null, list.Count);
+
+ return list;
+ }
+
+ /// <summary>申请任务分片</summary>
+ /// <param name="topic">主题</param>
+ /// <param name="server">申请任务的服务端</param>
+ /// <param name="ip">申请任务的IP</param>
+ /// <param name="pid">申请任务的服务端进程ID</param>
+ /// <param name="count">要申请的任务个数</param>
+ /// <param name="cache">缓存对象</param>
+ /// <returns></returns>
+ public IList<JobTask> AcquireMessage(Job job, String topic, String server, String ip, Int32 pid, Int32 count, ICache cache)
+ {
+ // 消费消息时,保存主题
+ if (job.Topic != topic)
+ {
+ job.Topic = topic;
+ job.SaveAsync();
+ }
+
+ var list = new List<JobTask>();
+
+ if (!job.Enable) return list;
+
+ // 验证消息数
+ var now = DateTime.Now;
+ if (job.MessageCount == 0 && job.UpdateTime.AddMinutes(2) > now) return list;
+
+ using var span = _tracer?.NewSpan(nameof(AcquireMessage), new { job.Name, topic, server, ip, pid, count });
+
+ using var ts = Job.Meta.CreateTrans();
+ var size = job.BatchSize;
+ if (size == 0) size = 1;
+
+ // 消费消息。请求任务数量=空闲线程*批大小
+ var msgs = AppMessage.GetTopic(job.AppID, topic, now, count * size);
+ if (msgs.Count > 0)
+ {
+ for (var i = 0; i < msgs.Count;)
+ {
+ var msgList = msgs.Skip(i).Take(size).ToList();
+ if (msgList.Count == 0) break;
+
+ i += msgList.Count;
+
+ // 创建新的分片
+ var ti = new JobTask
+ {
+ AppID = job.AppID,
+ JobID = job.ID,
+ Data = msgList.Select(e => e.Data).ToJson(),
+ MsgCount = msgList.Count,
+ BatchSize = size,
+
+ Server = server,
+ ProcessID = Interlocked.Increment(ref _idxBatch),
+ Client = $"{ip}@{pid}",
+ Status = JobStatus.就绪,
+ CreateTime = DateTime.Now,
+ UpdateTime = DateTime.Now
+ };
+
+ ti.Insert();
+
+ // 从去重缓存去掉
+ cache.Remove(msgList.Select(e => $"antjob:{job.AppID}:{job.Topic}:{e}").ToArray());
+
+ list.Add(ti);
+ }
+
+ // 批量删除消息
+ msgs.Delete();
+ }
+
+ // 更新作业下的消息数
+ job.MessageCount = AppMessage.FindCountByAppIDAndTopic(job.AppID, topic);
+ job.UpdateTime = now;
+ job.Save();
+
+ // 消费完成后,更新应用的消息数
+ if (job.MessageCount == 0)
+ {
+ var app = job.App;
+ if (app != null)
+ {
+ app.MessageCount = AppMessage.FindCountByAppID(job.ID);
+ app.SaveAsync();
+ }
+ }
+
+ ts.Commit();
+
+ // 记录任务数
+ span?.AppendTag(null, list.Count);
+
+ return list;
+ }
+ #endregion
+
+ #region 辅助
+ /// <summary>兼容旧数据库。如DataTime对应Start</summary>
+ public static void FixOld()
+ {
+ using var span = DefaultTracer.Instance?.NewSpan(nameof(FixOld));
+
+ var dal = DAL.Create("Ant");
+ FixOld(dal, "Job");
+ FixOld(dal, "JobTask");
+ FixOld(dal, "JobError");
+ }
+
+ /// <summary>兼容旧数据库。如DataTime对应Start</summary>
+ /// <param name="dal"></param>
+ /// <param name="tableName"></param>
+ public static void FixOld(DAL dal, String tableName)
+ {
+ var table = dal.Tables.FirstOrDefault(e => tableName.EqualIgnoreCase(e.Name, e.TableName));
+ if (table == null) return;
+
+ // 如果不包括Start列,则不需要处理
+ if (!table.Columns.Any(e => e.Name.EqualIgnoreCase("Start"))) return;
+ if (!table.Columns.Any(e => e.Name.EqualIgnoreCase("DataTime"))) return;
+
+ // 查询最后一批数据,更新字段值
+ var id = dal.Query<Int32>($"select id from {tableName} where (DataTime is null or DataTime<'2000-01-01') order by id desc", null, 0, 1).FirstOrDefault();
+ if (id == 0) return;
+
+ XTrace.WriteLine("数据表[{0}]最大Id是:{1},开始修正", tableName, id);
+ if (id > 100_000)
+ id -= 100_000;
+ else
+ id = 0;
+
+ var rs = dal.Execute($"update {tableName} set DataTime=Start where ID>{id} and (DataTime is null or DataTime<'2000-01-01')");
+ XTrace.WriteLine("从Id={0}开始,共修正{1}行", id, rs);
+ }
+ #endregion
}
diff --git a/AntJob.Server/Setting.cs b/AntJob.Server/Setting.cs
index c0fb289..394d2de 100644
--- a/AntJob.Server/Setting.cs
+++ b/AntJob.Server/Setting.cs
@@ -32,8 +32,8 @@ public class AntJobSetting : Config<AntJobSetting>
[Description("自动注册。任意应用登录时自动注册,省去人工配置应用账号的麻烦,默认true")]
public Boolean AutoRegistry { get; set; } = true;
- /// <summary>Redis缓存。设置用于控制任务切分的分布式锁,默认为空使用本进程内存锁</summary>
- [Description("Redis缓存。设置用于控制任务切分的分布式锁,默认为空使用本进程内存锁")]
- public String RedisCache { get; set; }
+ ///// <summary>Redis缓存。设置用于控制任务切分的分布式锁,默认为空使用本进程内存锁</summary>
+ //[Description("Redis缓存。设置用于控制任务切分的分布式锁,默认为空使用本进程内存锁")]
+ //public String RedisCache { get; set; }
#endregion
}
\ No newline at end of file
diff --git a/AntJob.Server/Worker.cs b/AntJob.Server/Worker.cs
index 90a4f7d..8c07a56 100644
--- a/AntJob.Server/Worker.cs
+++ b/AntJob.Server/Worker.cs
@@ -17,19 +17,24 @@ public class Worker : IHostedService
private readonly IRegistry _registry;
private readonly ICacheProvider _cacheProvider;
private readonly IServiceProvider _provider;
+ private readonly AntJobSetting _setting;
private readonly ITracer _tracer;
- public Worker(ICacheProvider cacheProvider, IServiceProvider provider, ITracer tracer)
+ public Worker(ICacheProvider cacheProvider, IServiceProvider provider, AntJobSetting setting, ITracer tracer)
{
_cacheProvider = cacheProvider;
_provider = provider;
+ _setting = setting;
_tracer = tracer;
_registry = provider.GetService<IRegistry>();
}
public async Task StartAsync(CancellationToken cancellationToken)
{
- var set = AntJobSetting.Current;
+ InitData();
+
+ //var set = AntJobSetting.Current;
+ var set = _setting;
// 实例化RPC服务端,指定端口,指定ServiceProvider,用于依赖注入获取接口服务层
var server = new ApiServer(set.Port)
@@ -46,18 +51,18 @@ public class Worker : IHostedService
// 本地结点
AntService.Local = new IPEndPoint(NetHelper.MyIP(), set.Port);
- // 数据缓存,也用于全局锁,支持MemoryCache和Redis
- if (_cacheProvider.Cache is not FullRedis && !set.RedisCache.IsNullOrEmpty())
- {
- var redis = new Redis { Timeout = 5_000 + 1_000 };
- redis.Init(set.RedisCache);
+ //// 数据缓存,也用于全局锁,支持MemoryCache和Redis
+ //if (_cacheProvider.Cache is not FullRedis && !set.RedisCache.IsNullOrEmpty())
+ //{
+ // var redis = new Redis { Timeout = 5_000 + 1_000 };
+ // redis.Init(set.RedisCache);
- _cacheProvider.Cache = redis;
- }
+ // _cacheProvider.Cache = redis;
+ //}
server.Start();
- _clearOnlineTimer = new TimerX(ClearOnline, null, 1000, 10 * 1000);
+ _clearOnlineTimer = new TimerX(ClearOnline, null, 1000, 10 * 1000) { Async = true };
_clearItemTimer = new TimerX(ClearItems, null, 10_000, 3600_000) { Async = true };
// 启用星尘注册中心,向注册中心注册服务,服务消费者将自动更新服务端地址列表
@@ -74,8 +79,6 @@ public class Worker : IHostedService
private static void InitData()
{
- var n = App.Meta.Count;
-
var set = NewLife.Setting.Current;
if (set.IsNew)
{
@@ -94,6 +97,8 @@ public class Worker : IHostedService
set2.Save();
}
+
+ _ = EntityFactory.InitAllAsync();
}
#region 清理过时
@@ -140,6 +145,9 @@ public class Worker : IHostedService
p += list.Count;
}
+ // 删除作业已不存在的任务
+ rs += JobTask.DeleteNoJob();
+
if (rs > 0)
{
sw.Stop();
diff --git a/AntJob.Web/AntJob.Web.csproj b/AntJob.Web/AntJob.Web.csproj
index c1a418b..a514b10 100644
--- a/AntJob.Web/AntJob.Web.csproj
+++ b/AntJob.Web/AntJob.Web.csproj
@@ -27,13 +27,13 @@
</ItemGroup>
<ItemGroup>
<Compile Remove="Areas\Ant\Controllers\AppConfigController.cs" />
+ <Compile Remove="Common\ApiFilterAttribute.cs" />
</ItemGroup>
<ItemGroup>
<Content Remove="Areas\Ant\Views\AppHistory\_List_Data.cshtml" />
- <Content Remove="Areas\Ant\Views\AppMessage\_List_Data.cshtml" />
- <Content Remove="Areas\Ant\Views\AppOnline\_List_Data.cshtml" />
<Content Remove="Areas\Ant\Views\App\_List_Data.cshtml" />
- <Content Remove="Areas\Ant\Views\JobError\_List_Data.cshtml" />
+ <Content Remove="Areas\Ant\Views\JobTask\_List_Data.cshtml" />
+ <Content Remove="Areas\Ant\Views\Job\_List_Data.cshtml" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\AntJob.Server\Services\AppService.cs" Link="Services\AppService.cs" />
@@ -44,15 +44,19 @@
<Content Include="favicon.ico" />
</ItemGroup>
<ItemGroup>
- <PackageReference Include="NewLife.Cube.Core" Version="6.1.2024.403" />
- <PackageReference Include="NewLife.Stardust.Extensions" Version="2.9.2024.402" />
+ <PackageReference Include="NewLife.Cube.Core" Version="6.1.2024.1005" />
+ <PackageReference Include="NewLife.Remoting.Extensions" Version="3.0.2024.1002" />
+ <PackageReference Include="NewLife.Stardust.Extensions" Version="3.1.2024.1004" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AntJob.Data\AntJob.Data.csproj" />
<ProjectReference Include="..\AntJob\AntJob.csproj" />
</ItemGroup>
<ItemGroup>
- <Folder Include="Areas\Ant\Views\AppOnline\" />
+ <Folder Include="Common\" />
<Folder Include="Services\" />
</ItemGroup>
+ <ItemGroup>
+ <None Include="Areas\Ant\Views\Shared\_App_Nav.cshtml" />
+ </ItemGroup>
</Project>
\ No newline at end of file
diff --git a/AntJob.Web/Areas/Ant/AntAreaRegistration.cs b/AntJob.Web/Areas/Ant/AntAreaRegistration.cs
index a38acc5..2987a64 100644
--- a/AntJob.Web/Areas/Ant/AntAreaRegistration.cs
+++ b/AntJob.Web/Areas/Ant/AntAreaRegistration.cs
@@ -1,16 +1,45 @@
-using System;
-using System.ComponentModel;
+using System.ComponentModel;
+using Microsoft.AspNetCore.Mvc.Filters;
using NewLife;
using NewLife.Cube;
+using NewLife.Cube.ViewModels;
+using XCode;
-namespace AntJob.Web.Areas.Ant
+namespace AntJob.Web.Areas.Ant;
+
+/// <summary>蚂蚁调度</summary>
+[DisplayName("蚂蚁调度")]
+public class AntArea : AreaBase
{
- /// <summary>蚂蚁调度</summary>
- [DisplayName("蚂蚁调度")]
- public class AntArea : AreaBase
+ public AntArea() : base(nameof(AntArea).TrimEnd("Area")) { }
+
+ static AntArea() => RegisterArea<AntArea>();
+}
+
+public class AntEntityController<T> : EntityController<T> where T : Entity<T>, new()
+{
+ public override void OnActionExecuting(ActionExecutingContext filterContext)
{
- public AntArea() : base(nameof(AntArea).TrimEnd("Area")) { }
+ base.OnActionExecuting(filterContext);
+
+ var appId = GetRequest("appId").ToInt(-1);
+ if (appId > 0)
+ {
+ PageSetting.NavView = "_App_Nav";
+ PageSetting.EnableNavbar = false;
+ }
+ }
+
+ protected override FieldCollection OnGetFields(ViewKinds kind, Object model)
+ {
+ var fields = base.OnGetFields(kind, model);
+
+ if (kind == ViewKinds.List)
+ {
+ var appId = GetRequest("appId").ToInt(-1);
+ if (appId > 0) fields.RemoveField("AppName");
+ }
- static AntArea() => RegisterArea<AntArea>();
+ return fields;
}
}
\ No newline at end of file
diff --git a/AntJob.Web/Areas/Ant/Controllers/AppController.cs b/AntJob.Web/Areas/Ant/Controllers/AppController.cs
index 406c450..7ec989c 100644
--- a/AntJob.Web/Areas/Ant/Controllers/AppController.cs
+++ b/AntJob.Web/Areas/Ant/Controllers/AppController.cs
@@ -1,10 +1,11 @@
using System.ComponentModel;
using AntJob.Data.Entity;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
using NewLife;
using NewLife.Cube;
+using NewLife.Cube.ViewModels;
using NewLife.Web;
-using XCode;
using XCode.Membership;
namespace AntJob.Web.Areas.Ant.Controllers;
@@ -13,14 +14,21 @@ namespace AntJob.Web.Areas.Ant.Controllers;
[AntArea]
[DisplayName("应用系统")]
[Menu(100)]
-public class AppController : EntityController<App>
+public class AppController : AntEntityController<App>
{
static AppController()
{
+ LogOnChange = true;
+
ListFields.RemoveField("UpdateUserID");
ListFields.RemoveCreateField().RemoveRemarkField();
{
+ var df = ListFields.GetField("Name") as ListField;
+ df.Url = "/Ant/App/Detail?id={ID}";
+ df.Target = "_blank";
+ }
+ {
var df = ListFields.AddListField("Online", "UpdateUser");
df.DisplayName = "在线";
df.Url = "/Ant/AppOnline?appid={ID}";
@@ -52,22 +60,35 @@ public class AppController : EntityController<App>
}
}
+ public override void OnActionExecuting(ActionExecutingContext filterContext)
+ {
+ base.OnActionExecuting(filterContext);
+
+ var appId = GetRequest("Id").ToInt(-1);
+ if (appId > 0)
+ {
+ PageSetting.NavView = "_App_Nav";
+ PageSetting.EnableNavbar = false;
+ }
+ }
+
/// <summary>搜索数据集</summary>
/// <param name="p"></param>
/// <returns></returns>
protected override IEnumerable<App> Search(Pager p)
{
var id = p["id"].ToInt(-1);
+ if (id <= 0) id = p["appId"].ToInt(-1);
if (id > 0)
{
- var list = new List<App>();
var entity = App.FindByID(id);
- if (entity != null) list.Add(entity);
-
- return list;
+ if (entity != null) return [entity];
}
- return App.Search(p["category"], p["Enable"]?.ToBoolean(), p["q"], p);
+ var category = p["category"];
+ var enable = p["Enable"]?.ToBoolean();
+
+ return App.Search(category, enable, p["q"], p);
}
protected override Int32 OnUpdate(App entity)
@@ -85,7 +106,7 @@ public class AppController : EntityController<App>
public ActionResult ResetApp()
{
var ids = GetRequest("keys").SplitAsInt();
- if (!ids.Any()) return JsonRefresh("未选中项!");
+ if (ids.Length == 0) return JsonRefresh("未选中项!");
var now = DateTime.Now;
foreach (var appid in ids)
@@ -94,7 +115,7 @@ public class AppController : EntityController<App>
var jobs = Job.FindAllByAppID2(appid);
foreach (var job in jobs)
{
- job.Start = new DateTime(now.Year, now.Month, 1);
+ job.DataTime = new DateTime(now.Year, now.Month, 1);
job.ResetOther();
}
@@ -111,68 +132,4 @@ public class AppController : EntityController<App>
return JsonRefresh("操作完毕!");
}
-
- /// <summary>启用禁用任务</summary>
- /// <param name="id"></param>
- /// <param name="enable"></param>
- /// <returns></returns>
- [EntityAuthorize(PermissionFlags.Update)]
- public ActionResult Set(Int32 id = 0, Boolean enable = true)
- {
- if (id > 0)
- {
- var dt = App.FindByID(id);
- if (dt == null) throw new ArgumentNullException(nameof(id), "找不到任务 " + id);
-
- dt.Enable = enable;
- dt.Save();
- }
- else
- {
- var ids = GetRequest("keys").SplitAsInt();
-
- foreach (var item in ids)
- {
- var dt = App.FindByID(item);
- if (dt != null && dt.Enable != enable)
- {
- dt.Enable = enable;
- dt.Save();
- }
- }
- }
- return JsonRefresh("操作成功!");
- }
-
- protected override Boolean Valid(App entity, DataObjectMethodType type, Boolean post)
- {
- if (!post) return base.Valid(entity, type, post);
-
- var act = type switch
- {
- DataObjectMethodType.Update => "修改",
- DataObjectMethodType.Insert => "添加",
- DataObjectMethodType.Delete => "删除",
- _ => type + "",
- };
-
- // 必须提前写修改日志,否则修改后脏数据失效,保存的日志为空
- if (type == DataObjectMethodType.Update && (entity as IEntity).HasDirty)
- LogProvider.Provider.WriteLog(act, entity);
-
- var err = "";
- try
- {
- return base.Valid(entity, type, post);
- }
- catch (Exception ex)
- {
- err = ex.Message;
- throw;
- }
- finally
- {
- LogProvider.Provider.WriteLog(act, entity, err);
- }
- }
}
\ No newline at end of file
diff --git a/AntJob.Web/Areas/Ant/Controllers/AppHistoryController.cs b/AntJob.Web/Areas/Ant/Controllers/AppHistoryController.cs
index 80d1926..8201fbf 100644
--- a/AntJob.Web/Areas/Ant/Controllers/AppHistoryController.cs
+++ b/AntJob.Web/Areas/Ant/Controllers/AppHistoryController.cs
@@ -2,6 +2,7 @@
using AntJob.Data.Entity;
using NewLife.Cube;
using NewLife.Cube.Extensions;
+using NewLife.Cube.ViewModels;
using NewLife.Web;
namespace AntJob.Web.Areas.Ant.Controllers;
@@ -10,13 +11,14 @@ namespace AntJob.Web.Areas.Ant.Controllers;
[AntArea]
[DisplayName("应用历史")]
[Menu(0, false)]
-public class AppHistoryController : EntityController<AppHistory>
+public class AppHistoryController : AntEntityController<AppHistory>
{
static AppHistoryController()
{
- //MenuOrder = 85;
+ //AppHistory.Meta.Table.DataTable.InsertOnly = true;
- AppOnline.Meta.Table.DataTable.InsertOnly = true;
+ ListFields.RemoveField("Id", "Version", "CompileTime", "");
+ ListFields.AddListField("Remark", null, "TraceId");
ListFields.TraceUrl();
}
@@ -26,6 +28,8 @@ public class AppHistoryController : EntityController<AppHistory>
/// <returns></returns>
protected override IEnumerable<AppHistory> Search(Pager p)
{
+ PageSetting.EnableAdd = false;
+
var appid = p["appid"].ToInt(-1);
var act = p["action"];
var success = p["success"]?.ToBoolean();
diff --git a/AntJob.Web/Areas/Ant/Controllers/AppMessageController.cs b/AntJob.Web/Areas/Ant/Controllers/AppMessageController.cs
index 9642420..6ca29f1 100644
--- a/AntJob.Web/Areas/Ant/Controllers/AppMessageController.cs
+++ b/AntJob.Web/Areas/Ant/Controllers/AppMessageController.cs
@@ -2,6 +2,7 @@
using AntJob.Data.Entity;
using NewLife.Cube;
using NewLife.Cube.Extensions;
+using NewLife.Cube.ViewModels;
using NewLife.Web;
namespace AntJob.Web.Areas.Ant.Controllers;
@@ -10,11 +11,21 @@ namespace AntJob.Web.Areas.Ant.Controllers;
[AntArea]
[DisplayName("应用消息")]
[Menu(70)]
-public class AppMessageController : EntityController<AppMessage>
+public class AppMessageController : AntEntityController<AppMessage>
{
- //static AppMessageController() => MenuOrder = 49;
+ static AppMessageController()
+ {
+ LogOnChange = true;
+
+ ListFields.RemoveField("CreateIP", "UpdateIP");
- static AppMessageController() => ListFields.TraceUrl();
+ {
+ var df = ListFields.AddListField("Data", null, "Topic");
+ df.TextAlign = TextAligns.Nowrap;
+ }
+
+ ListFields.TraceUrl();
+ }
/// <summary>搜索数据集</summary>
/// <param name="p"></param>
diff --git a/AntJob.Web/Areas/Ant/Controllers/AppOnlineController.cs b/AntJob.Web/Areas/Ant/Controllers/AppOnlineController.cs
index cd97ff9..ab6a6db 100644
--- a/AntJob.Web/Areas/Ant/Controllers/AppOnlineController.cs
+++ b/AntJob.Web/Areas/Ant/Controllers/AppOnlineController.cs
@@ -10,13 +10,13 @@ namespace AntJob.Web.Areas.Ant.Controllers;
[AntArea]
[DisplayName("应用在线")]
[Menu(80)]
-public class AppOnlineController : EntityController<AppOnline>
+public class AppOnlineController : AntEntityController<AppOnline>
{
static AppOnlineController()
{
- //MenuOrder = 90;
+ //AppOnline.Meta.Table.DataTable.InsertOnly = true;
- AppOnline.Meta.Table.DataTable.InsertOnly = true;
+ ListFields.RemoveField("End");
ListFields.TraceUrl();
}
@@ -26,6 +26,8 @@ public class AppOnlineController : EntityController<AppOnline>
/// <returns></returns>
protected override IEnumerable<AppOnline> Search(Pager p)
{
+ PageSetting.EnableAdd = false;
+
var appid = p["appid"].ToInt(-1);
var start = p["dtStart"].ToDateTime();
var end = p["dtEnd"].ToDateTime();
diff --git a/AntJob.Web/Areas/Ant/Controllers/JobController.cs b/AntJob.Web/Areas/Ant/Controllers/JobController.cs
index 37c6a80..aa3d2cc 100644
--- a/AntJob.Web/Areas/Ant/Controllers/JobController.cs
+++ b/AntJob.Web/Areas/Ant/Controllers/JobController.cs
@@ -1,11 +1,13 @@
using System.ComponentModel;
+using AntJob.Data;
using AntJob.Data.Entity;
using Microsoft.AspNetCore.Mvc;
using NewLife;
using NewLife.Cube;
+using NewLife.Cube.ViewModels;
+using NewLife.Data;
using NewLife.Security;
using NewLife.Web;
-using XCode;
using XCode.Membership;
namespace AntJob.Web.Areas.Ant.Controllers;
@@ -14,28 +16,164 @@ namespace AntJob.Web.Areas.Ant.Controllers;
[AntArea]
[DisplayName("作业")]
[Menu(0, false)]
-public class JobController : EntityController<Job>
+public class JobController : AntEntityController<Job>
{
static JobController()
{
- var list = ListFields;
- list.RemoveField("Type");
- list.RemoveField("CreateUserID");
- list.RemoveField("CreateTime");
- list.RemoveField("CreateIP");
- list.RemoveField("UpdateUserID");
- list.RemoveField("UpdateIP");
-
- //MenuOrder = 80;
+ LogOnChange = true;
+
+ ListFields.RemoveField("ClassName", "Step", "Cron", "Topic", "MessageCount", "Time", "End");
+ ListFields.RemoveField("Times", "Speed");
+ ListFields.RemoveField("MaxError", "MaxRetry", "MaxTime", "MaxRetain", "MaxIdle", "ErrorDelay", "Deadline");
+ ListFields.RemoveCreateField().RemoveUpdateField();
+ ListFields.AddListField("UpdateTime");
+
+ {
+ var df = ListFields.GetField("Name") as ListField;
+ df.Url = "/Ant/JobTask?appid={AppID}&jobId={ID}";
+ }
+ //{
+ // var df = ListFields.AddListField("Task", "Enable");
+ // df.DisplayName = "任务";
+ // df.Url = "/Ant/JobTask?appid={AppID}&jobId={ID}";
+ //}
+ {
+ var df = ListFields.AddListField("Title", null, "Mode");
+ df.Header = "下一次/Cron/主题";
+ df.HeaderTitle = "Cron格式,秒+分+时+天+月+星期+年";
+ df.AddService(new MyTextField());
+ }
+ {
+ var df = ListFields.GetField("DataTime") as ListField;
+ //df.GetClass = e => "text-center text-primary font-weight-bold";
+ df.AddService(new ColorField { Color = "Magenta", GetValue = e => ((DateTime)e).ToFullString("") });
+ }
+ //{
+ // var df = ListFields.GetField("Step");
+ // df.DataVisible = e => (e as Job).Mode == JobModes.Data;
+ //}
+ {
+ var df = ListFields.GetField("BatchSize");
+ df.DataVisible = e => (e as Job).Mode != JobModes.Time;
+ }
+ //{
+ // var df = ListFields.GetField("MaxTask");
+ // df.DataVisible = e => (e as Job).Mode != JobModes.Message;
+ //}
+ {
+ var df = ListFields.GetField("Success");
+ df.AddService(new ColorNumberField { Color = "green" });
+ }
+ {
+ var df = ListFields.GetField("Error");
+ df.AddService(new ColorNumberField { Color = "red" });
+ }
+ {
+ var df = ListFields.GetField("LastStatus") as ListField;
+ df.GetClass = e =>
+ {
+ var job = e as Job;
+ return job.LastStatus switch
+ {
+ JobStatus.就绪 => "text-center",
+ JobStatus.抽取中 => "text-center info",
+ JobStatus.处理中 => "text-center warning",
+ JobStatus.错误 => "text-center danger",
+ JobStatus.完成 => "text-center success",
+ JobStatus.取消 => "text-center active",
+ JobStatus.延迟 => "text-center active",
+ _ => "",
+ };
+ };
+ }
+ }
+
+ class MyTextField : ILinkExtend
+ {
+ public String Resolve(DataField field, IModel data)
+ {
+ var job = data as Job;
+ return job.Mode switch
+ {
+ JobModes.Data => $"<font color=blue><b>{job.DataTime.ToFullString("")}</b></font>",
+ JobModes.Time => $"<font color=DarkVoilet><b>{job.Cron}</b></font>",
+ JobModes.Message => $"<font color=green><b>{job.Topic}</b></font>",
+ //JobModes.CSharp => "[C#]" + job.Time.ToFullString(""),
+ //JobModes.Sql => "[Sql]" + job.Time.ToFullString(""),
+ _ => $"<b>{job.DataTime.ToFullString("")}</b>",
+ };
+ }
+ }
+
+ class ColorField : ILinkExtend
+ {
+ public String Color { get; set; }
+
+ public Func<Object, String> GetValue;
+
+ public String Resolve(DataField field, IModel data)
+ {
+ if (data is Job job)
+ {
+ if (job.Mode == JobModes.Message) return "";
+ if (job.Mode == JobModes.Data) return $"+{TimeSpan.FromSeconds(job.Step)}";
+ }
+
+ var value = data[field.Name];
+ if (GetValue != null) value = GetValue(value);
+ return $"<font color={Color}><b>{value}</b></font>";
+ }
+ }
+
+ class ColorNumberField : ILinkExtend
+ {
+ public String Color { get; set; }
+
+ public String Resolve(DataField field, IModel data)
+ {
+ var value = data[field.Name];
+ return $"<font color={Color}><b>{value:n0}</b></font>";
+ }
}
- public JobController() => PageSetting.EnableAdd = false;
+ protected override FieldCollection OnGetFields(ViewKinds kind, Object model)
+ {
+ var fs = base.OnGetFields(kind, model);
+ if (model is not Job job) return fs;
+
+ if (kind is ViewKinds.EditForm or ViewKinds.Detail)
+ {
+ // Cron/Topic/MessageCount/End/Step/Offset/BatchSize
+ switch (job.Mode)
+ {
+ case JobModes.Data:
+ fs.RemoveField("Topic", "MessageCount", "Cron");
+ break;
+ case JobModes.Time:
+ fs.RemoveField("Topic", "MessageCount", "Step", "BatchSize");
+ break;
+ case JobModes.Message:
+ fs.RemoveField("Cron", "End", "Step", "Offset");
+ break;
+ default:
+ break;
+ }
+ }
+ else if (kind is ViewKinds.AddForm)
+ {
+ fs.RemoveField("Cron", "Topic", "MessageCount", "End", "Step", "Offset", "BatchSize");
+ }
+
+ return fs;
+ }
/// <summary>搜索数据集</summary>
/// <param name="p"></param>
/// <returns></returns>
protected override IEnumerable<Job> Search(Pager p)
{
+ PageSetting.EnableAdd = false;
+
var id = p["ID"].ToInt(-1);
var appid = p["appid"].ToInt(-1);
var start = p["dtStart"].ToDateTime();
@@ -45,38 +183,6 @@ public class JobController : EntityController<Job>
return Job.Search(id, appid, start, end, mode, p["q"], p);
}
- /// <summary>启用禁用任务</summary>
- /// <param name="id"></param>
- /// <param name="enable"></param>
- /// <returns></returns>
- [EntityAuthorize(PermissionFlags.Update)]
- public ActionResult Set(Int32 id = 0, Boolean enable = true)
- {
- if (id > 0)
- {
- var dt = Job.FindByID(id);
- if (dt == null) throw new ArgumentNullException(nameof(id), "找不到任务 " + id);
-
- dt.Enable = enable;
- dt.Save();
- }
- else
- {
- var ids = GetRequest("keys").SplitAsInt();
-
- foreach (var item in ids)
- {
- var dt = Job.FindByID(item);
- if (dt != null && dt.Enable != enable)
- {
- dt.Enable = enable;
- dt.Save();
- }
- }
- }
- return JsonRefresh("操作成功!");
- }
-
/// <summary>
/// 重置时间
/// </summary>
@@ -174,36 +280,4 @@ public class JobController : EntityController<Job>
// 跳转到编辑页,这里时候已经得到新的自增ID
return Edit(job.ID + "");
}
-
- protected override Boolean Valid(Job entity, DataObjectMethodType type, Boolean post)
- {
- if (!post) return base.Valid(entity, type, post);
-
- var act = type switch
- {
- DataObjectMethodType.Update => "修改",
- DataObjectMethodType.Insert => "添加",
- DataObjectMethodType.Delete => "删除",
- _ => type + "",
- };
-
- // 必须提前写修改日志,否则修改后脏数据失效,保存的日志为空
- if (type == DataObjectMethodType.Update && (entity as IEntity).HasDirty)
- LogProvider.Provider.WriteLog(act, entity);
-
- var err = "";
- try
- {
- return base.Valid(entity, type, post);
- }
- catch (Exception ex)
- {
- err = ex.Message;
- throw;
- }
- finally
- {
- LogProvider.Provider.WriteLog(act, entity, err);
- }
- }
}
\ No newline at end of file
diff --git a/AntJob.Web/Areas/Ant/Controllers/JobErrorController.cs b/AntJob.Web/Areas/Ant/Controllers/JobErrorController.cs
index 6234994..06f1472 100644
--- a/AntJob.Web/Areas/Ant/Controllers/JobErrorController.cs
+++ b/AntJob.Web/Areas/Ant/Controllers/JobErrorController.cs
@@ -2,6 +2,7 @@
using AntJob.Data.Entity;
using NewLife.Cube;
using NewLife.Cube.Extensions;
+using NewLife.Cube.ViewModels;
using NewLife.Web;
namespace AntJob.Web.Areas.Ant.Controllers;
@@ -10,12 +11,13 @@ namespace AntJob.Web.Areas.Ant.Controllers;
[AntArea]
[DisplayName("作业错误")]
[Menu(0, false)]
-public class JobErrorController : EntityController<JobError>
+public class JobErrorController : AntEntityController<JobError>
{
- //static JobErrorController() => MenuOrder = 60;
-
static JobErrorController()
{
+ ListFields.RemoveField("DataTime", "End");
+ ListFields.AddListField("Message", null, "TraceId");
+
ListFields.TraceUrl();
}
@@ -24,6 +26,8 @@ public class JobErrorController : EntityController<JobError>
/// <returns></returns>
protected override IEnumerable<JobError> Search(Pager p)
{
+ PageSetting.EnableAdd = false;
+
var appid = p["appid"].ToInt(-1);
var jobid = p["JobID"].ToInt(-1);
var start = p["dtStart"].ToDateTime();
diff --git a/AntJob.Web/Areas/Ant/Controllers/JobTaskController.cs b/AntJob.Web/Areas/Ant/Controllers/JobTaskController.cs
index 95b134c..72733e4 100644
--- a/AntJob.Web/Areas/Ant/Controllers/JobTaskController.cs
+++ b/AntJob.Web/Areas/Ant/Controllers/JobTaskController.cs
@@ -1,10 +1,14 @@
using System.ComponentModel;
+using System.Web;
using AntJob.Data;
using AntJob.Data.Entity;
+using AntJob.Server.Services;
using Microsoft.AspNetCore.Mvc;
using NewLife;
using NewLife.Cube;
using NewLife.Cube.Extensions;
+using NewLife.Cube.ViewModels;
+using NewLife.Data;
using NewLife.Web;
using XCode.Membership;
@@ -14,29 +18,137 @@ namespace AntJob.Web.Areas.Ant.Controllers;
[AntArea]
[DisplayName("作业任务")]
[Menu(0, false)]
-public class JobTaskController : EntityController<JobTask>
+public class JobTaskController : AntEntityController<JobTask>
{
+ private readonly JobService _jobService;
+
static JobTaskController()
{
- //MenuOrder = 70;
+ LogOnChange = true;
+
+ ListFields.RemoveField("Server", "ProcessID", "End");
+ ListFields.RemoveCreateField();
+
+ {
+ var df = ListFields.GetField("DataTime") as ListField;
+ df.Header = "时间/数据";
+ df.AddService(new MyTitleField());
+ }
+ {
+ var df = ListFields.GetField("Success") as ListField;
+ df.AddService(new ColorNumberField { Color = "green" });
+ //df.GetValue = e => $"<font color=green><b>{(e as IModel)["Success"]:n0}</b></font>";
+ }
+ {
+ var df = ListFields.GetField("Error");
+ df.DataVisible = e => (e as JobTask).Error > 0;
+ df.AddService(new ColorNumberField { Color = "red" });
+ }
+ {
+ var df = ListFields.GetField("Status") as ListField;
+ //df.AddService(new MyStatusField());
+ df.GetClass = e =>
+ {
+ var job = e as JobTask;
+ return job.Status switch
+ {
+ JobStatus.就绪 => "text-center",
+ JobStatus.抽取中 => "text-center info",
+ JobStatus.处理中 => "text-center warning",
+ JobStatus.错误 => "text-center danger",
+ JobStatus.完成 => "text-center success",
+ JobStatus.取消 => "text-center active",
+ JobStatus.延迟 => "text-center active",
+ _ => "",
+ };
+ };
+ }
ListFields.TraceUrl();
}
+ class MyTitleField : ILinkExtend
+ {
+ public String Resolve(DataField field, IModel data)
+ {
+ var task = data as JobTask;
+ var mode = task?.Job?.Mode ?? JobModes.Time;
+ return mode switch
+ {
+ JobModes.Data => $"<font color=blue><b>({task.DataTime:MM-dd HH:mm:ss} - {task.End:HH:mm:ss})</b></font>",
+ JobModes.Time => $"<font color=DarkVoilet><b>{task.DataTime.ToFullString()}</b></font>",
+ JobModes.Message => $"<font color=green title=\"{HttpUtility.HtmlEncode(task.Data)}\"><b>{task.Data?.Cut(64, "..")}</b></font>",
+ _ => $"<b>{task.DataTime.ToFullString("")}</b>",
+ };
+ }
+ }
+
+ class ColorField : ILinkExtend
+ {
+ public String Color { get; set; }
+
+ public Func<Object, String> GetValue;
+
+ public String Resolve(DataField field, IModel data)
+ {
+ var value = data[field.Name];
+ if (GetValue != null) value = GetValue(value);
+ return $"<font color={Color}><b>{value}</b></font>";
+ }
+ }
+
+ class ColorNumberField : ILinkExtend
+ {
+ public String Color { get; set; }
+
+ public String Resolve(DataField field, IModel data)
+ {
+ var value = data[field.Name];
+ return $"<font color={Color}><b>{value:n0}</b></font>";
+ }
+ }
+
+ public JobTaskController(JobService jobService) => _jobService = jobService;
+
+ protected override FieldCollection OnGetFields(ViewKinds kind, Object model)
+ {
+ var fs = base.OnGetFields(kind, model);
+ if (kind == ViewKinds.List)
+ {
+ var appId = GetRequest("appId").ToInt();
+ if (appId > 0) fs.RemoveField("AppID", "AppName");
+
+ var jobId = GetRequest("jobId").ToInt();
+ if (jobId > 0) fs.RemoveField("JobID", "JobName");
+ }
+
+ return fs;
+ }
+
/// <summary>搜索数据集</summary>
/// <param name="p"></param>
/// <returns></returns>
protected override IEnumerable<JobTask> Search(Pager p)
{
+ PageSetting.EnableAdd = false;
+
var id = p["id"].ToInt(-1);
var jobid = p["JobID"].ToInt(-1);
var appid = p["AppID"].ToInt(-1);
var status = (JobStatus)p["Status"].ToInt(-1);
+ var client = p["Client"];
+
+ var dataStart = p["dataStart"].ToDateTime();
+ var dataEnd = p["dataEnd"].ToDateTime();
var start = p["dtStart"].ToDateTime();
var end = p["dtEnd"].ToDateTime();
- var client = p["Client"];
- return JobTask.Search(id, appid, jobid, status, start, end, client, p["q"], p);
+ if (jobid > 0)
+ {
+ ListFields.RemoveField("JobID");
+ }
+
+ return JobTask.Search(id, appid, jobid, status, dataStart, dataEnd, start, end, client, p["q"], p);
}
/// <summary>修改状态</summary>
@@ -45,32 +157,23 @@ public class JobTaskController : EntityController<JobTask>
[EntityAuthorize(PermissionFlags.Update)]
public ActionResult Set(Int32 id = 0)
{
- if (id > 0)
+ var rs = 0;
+ var ids = GetRequest("keys").SplitAsInt();
+ foreach (var item in ids)
{
- var dt = JobTask.FindByID(id);
- if (dt == null) throw new ArgumentNullException(nameof(id), "找不到任务 " + id);
-
- dt.Status = JobStatus.取消;
- if (dt.Times >= 10) dt.Times = 0;
-
- dt.Save();
- }
- else
- {
- var ids = GetRequest("keys").SplitAsInt();
-
- foreach (var item in ids)
+ var task = JobTask.FindByID(item);
+ if (task != null)
{
- var dt = JobTask.FindByID(item);
- if (dt != null)
- {
- dt.Status = JobStatus.取消;
- if (dt.Times >= 10) dt.Times = 0;
+ task.Status = JobStatus.取消;
+ if (task.Times >= 10) task.Times = 0;
+
+ rs += task.Save();
- dt.Save();
- }
+ // 提醒调度,马上放行
+ _jobService.SetDelay(task.JobID, DateTime.Now);
}
}
- return JsonRefresh("操作成功!");
+
+ return JsonRefresh($"操作成功!rs={rs}");
}
}
\ No newline at end of file
diff --git a/AntJob.Web/Areas/Ant/Views/_ViewImports.cshtml b/AntJob.Web/Areas/Ant/Views/_ViewImports.cshtml
index 39bae8f..6fd93cd 100644
--- a/AntJob.Web/Areas/Ant/Views/_ViewImports.cshtml
+++ b/AntJob.Web/Areas/Ant/Views/_ViewImports.cshtml
@@ -5,4 +5,7 @@
@using NewLife.Web
@using XCode
@using XCode.Membership
-@using AntJob.Data.Entity
\ No newline at end of file
+@using AntJob.Data.Entity
+@using NewLife.Cube.Areas.Admin.Models
+@using NewLife.Cube.Extensions
+@using NewLife.Cube.ViewModels
\ No newline at end of file
diff --git a/AntJob.Web/Areas/Ant/Views/JobTask/_DataRange.cshtml b/AntJob.Web/Areas/Ant/Views/JobTask/_DataRange.cshtml
new file mode 100644
index 0000000..bc5bac2
--- /dev/null
+++ b/AntJob.Web/Areas/Ant/Views/JobTask/_DataRange.cshtml
@@ -0,0 +1,42 @@
+@using NewLife;
+@{
+ var fmt = Model as String;
+ var formatStr = !fmt.IsNullOrEmpty() ? fmt : "yyyy-MM-dd";
+ var p = ViewBag.Page as Pager;
+
+ var dataStart = p["dataStart"].ToDateTime();
+ var dataEnd = p["dataEnd"].ToDateTime();
+ var step = (Int32)(dataEnd - dataStart).TotalDays + 1;
+}
+<div class="form-group">
+ <label for="dataStart" class="control-label">数据时间:</label>
+ <div class="input-group">
+ @if (formatStr == "yyyy-MM-dd" && (dataStart.Year > 2000 || dataEnd.Year > 2000))
+ {
+ var url = p.GetBaseUrl(true, true, true, new[] { "dataStart", "dataEnd" });
+ if (dataStart.Year > 2000 && dataEnd.Year > 2000) url.UrlParam("dataStart", dataStart.AddDays(-step).ToString("yyyy-MM-dd"));
+ if (dataStart.Year > 2000 && dataEnd.Year > 2000) url.UrlParam("dataEnd", dataEnd.AddDays(-step).ToString("yyyy-MM-dd"));
+ <span class="input-group-addon"><a href="?@Html.Raw(url)" title="前一段"><i class="fa fa-calendar"></i></a></span>
+ }
+ else
+ {
+ <span class="input-group-addon"><i class="fa fa-calendar"></i></span>
+ }
+ <input name="dataStart" id="dataStart" value="@p["dataStart"]" dateformat="@formatStr" class="form-control form_datetime" autocomplete="off" />
+ </div>
+ @if (formatStr == "yyyy-MM-dd" && (dataStart.Year > 2000 || dataEnd.Year > 2000))
+ {
+ var url = p.GetBaseUrl(true, true, true, new[] { "dataStart", "dataEnd" });
+ if (dataStart.Year > 2000 && dataEnd.Year > 2000) url.UrlParam("dataStart", dataStart.AddDays(step).ToString("yyyy-MM-dd"));
+ if (dataStart.Year > 2000 && dataEnd.Year > 2000) url.UrlParam("dataEnd", dataEnd.AddDays(step).ToString("yyyy-MM-dd"));
+ <label for="dataEnd" class="control-label"><a href="?@Html.Raw(url)" title="后一段">~</a></label>
+ }
+ else
+ {
+ <label for="dataEnd" class="control-label">~</label>
+ }
+ <div class="input-group">
+ @*<span class="input-group-addon"><i class="fa fa-calendar"></i></span>*@
+ <input name="dataEnd" id="dataEnd" value="@p["dataEnd"]" dateformat="@formatStr" class="form-control form_datetime" autocomplete="off" />
+ </div>
+</div>
\ No newline at end of file
diff --git a/AntJob.Web/Areas/Ant/Views/Shared/_App_Nav.cshtml b/AntJob.Web/Areas/Ant/Views/Shared/_App_Nav.cshtml
new file mode 100644
index 0000000..9716979
--- /dev/null
+++ b/AntJob.Web/Areas/Ant/Views/Shared/_App_Nav.cshtml
@@ -0,0 +1,61 @@
+@using AntJob.Data;
+@{
+ var path = Context.Request.Path + "";
+ var path2 = "/" + path?.Split("/").Take(3).Join("/");
+
+ var appId = Context.Request.Query["appId"].ToInt(0);
+ if (appId == 0 && path2.EqualIgnoreCase("/Ant/App"))
+ {
+ appId = Context.Request.Query["Id"].ToInt(0);
+ }
+
+ var start = Context.Request.Query["dtStart"].ToDateTime();
+ var end = Context.Request.Query["dtEnd"].ToDateTime();
+ if (end.Year < 2000) end = Context.Request.Query["dtEnd2"].ToDateTime().AddSeconds(1);
+
+ var app = App.FindByID(appId);
+
+ var dic = new Dictionary<String, Object>();
+ dic[app?.Name + ""] = "/Ant/App/Edit?Id=" + appId;
+ dic["作业"] = "/Ant/Job?appId=" + appId;
+ dic["任务"] = "/Ant/JobTask?appId=" + appId;
+ dic["消息"] = "/Ant/AppMessage?appId=" + appId;
+ dic["在线"] = "/Ant/AppOnline?appId=" + appId;
+ dic["历史"] = "/Ant/AppHistory?appId=" + appId;
+ dic["错误"] = "/Ant/JobError?appId=" + appId;
+
+}
+@if (appId > 0)
+{
+ <div class="navbar-collapse text-center">
+ <ul class="nav nav-pills" style="margin-bottom: 10px; display: inline-block;float: none;">
+ @foreach (var item in dic)
+ {
+ if (item.Value is IDictionary<String, Object> childs)
+ {
+ <li role="presentation" class="dropdown">
+ <a class="dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">@item.Key <span class="caret"></span></a>
+ <ul class="dropdown-menu">
+ @foreach (var elm in childs)
+ {
+ var url = elm.Value + "";
+ var v = url.Substring(null, "?");
+ <li role="presentation" class="@(path.EqualIgnoreCase(v)?"active":"")">
+ <a href="@url">@elm.Key</a>
+ </li>
+ }
+ </ul>
+ </li>
+ }
+ else
+ {
+ var url = item.Value + "";
+ var v = url.Substring(null, "?");
+ <li role="presentation" class="@(path.EqualIgnoreCase(v)?"active":"")">
+ <a href="@url">@item.Key</a>
+ </li>
+ }
+ }
+ </ul>
+ </div>
+}
\ No newline at end of file
diff --git a/AntJob.Web/Controllers/AntJobController.cs b/AntJob.Web/Controllers/AntJobController.cs
index 0e5c098..8c03bd1 100644
--- a/AntJob.Web/Controllers/AntJobController.cs
+++ b/AntJob.Web/Controllers/AntJobController.cs
@@ -4,19 +4,19 @@ using AntJob.Data.Entity;
using AntJob.Models;
using AntJob.Server;
using AntJob.Server.Services;
-using AntJob.Web.Common;
using AntJob.Web.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using NewLife;
-using NewLife.Caching;
using NewLife.Cube;
using NewLife.Log;
using NewLife.Remoting;
+using NewLife.Remoting.Models;
using NewLife.Serialization;
using NewLife.Web;
+using ApiFilterAttribute = NewLife.Remoting.Extensions.ApiFilterAttribute;
using IActionFilter = Microsoft.AspNetCore.Mvc.Filters.IActionFilter;
namespace AntJob.Web.Controllers;
@@ -36,7 +36,6 @@ public class AntJobController : ControllerBase, IActionFilter
private AntJobSetting _setting;
private readonly AppService _appService;
private readonly JobService _jobService;
- private readonly ICacheProvider _cacheProvider;
#region 构造
public AntJobController(AppService appService, JobService jobService, AntJobSetting setting)
@@ -103,9 +102,9 @@ public class AntJobController : ControllerBase, IActionFilter
[HttpPost(nameof(Login))]
public LoginResponse Login(LoginModel model)
{
- if (model.User.IsNullOrEmpty()) throw new ArgumentNullException(nameof(model.User));
+ if (model.Code.IsNullOrEmpty()) throw new ArgumentNullException(nameof(model.Code));
- var (app, rs) = _appService.Login(model, UserHost);
+ var (app, online, rs) = _appService.Login(model, UserHost);
return rs;
}
@@ -125,7 +124,7 @@ public class AntJobController : ControllerBase, IActionFilter
// 密码模式
if (model.grant_type == "password")
{
- var (app, rs) = _appService.Login(new LoginModel { User = model.UserName, Pass = model.Password }, ip);
+ var (app, online, rs) = _appService.Login(new LoginModel { Code = model.UserName, Secret = model.Password }, ip);
var tokenModel = _appService.IssueToken(app.Name, set);
diff --git a/AntJob.Web/Program.cs b/AntJob.Web/Program.cs
index 503fd72..d82983f 100644
--- a/AntJob.Web/Program.cs
+++ b/AntJob.Web/Program.cs
@@ -1,7 +1,4 @@
-using System;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.Extensions.Hosting;
-using NewLife.Cube;
+using NewLife.Cube;
using NewLife.Log;
namespace AntJob.Web;
@@ -12,11 +9,7 @@ public class Program
{
XTrace.UseConsole();
- var app = ApplicationManager.Load();
- do
- {
- app.Start(CreateHostBuilder(args).Build());
- } while (app.Restarting);
+ CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(String[] args)
diff --git a/AntJob.Web/Startup.cs b/AntJob.Web/Startup.cs
index 4ab7f35..672f02a 100644
--- a/AntJob.Web/Startup.cs
+++ b/AntJob.Web/Startup.cs
@@ -1,6 +1,7 @@
using AntJob.Server;
using AntJob.Server.Services;
using NewLife.Cube;
+using NewLife.Security;
using XCode;
namespace AntJob.Web;
@@ -28,6 +29,9 @@ public class Startup
services.AddSingleton<AppService>();
services.AddSingleton<JobService>();
+ // 注册密码提供者,用于通信过程中保护密钥,避免明文传输
+ services.AddSingleton<IPasswordProvider>(new SaltPasswordProvider { Algorithm = "md5", SaltTime = 60 });
+
services.AddControllersWithViews();
services.AddCube();
}
@@ -40,6 +44,9 @@ public class Startup
EntityFactory.InitConnection("Cube");
EntityFactory.InitConnection("Ant");
+ // 修正旧版数据
+ Task.Run(() => JobService.FixOld());
+
// 使用Cube前添加自己的管道
if (env.IsDevelopment())
app.UseDeveloperExceptionPage();
@@ -63,6 +70,6 @@ public class Startup
// 启用星尘注册中心,向注册中心注册服务,服务消费者将自动更新服务端地址列表
app.RegisterService("AntWeb", null, env.EnvironmentName);
- app.RegisterService("AntServer", null, env.EnvironmentName);
+ //app.RegisterService("AntServer", null, env.EnvironmentName);
}
}
\ No newline at end of file
diff --git a/AntJob/AntJob.csproj b/AntJob/AntJob.csproj
index e10b931..4970646 100644
--- a/AntJob/AntJob.csproj
+++ b/AntJob/AntJob.csproj
@@ -6,7 +6,7 @@
<Description>分布式任务调度系统,纯NET打造的重量级大数据实时计算平台,万亿级调度经验积累。</Description>
<Company>新生命开发团队</Company>
<Copyright>©2002-2024 NewLife</Copyright>
- <VersionPrefix>3.4</VersionPrefix>
+ <VersionPrefix>4.0</VersionPrefix>
<VersionSuffix>$([System.DateTime]::Now.ToString(`yyyy.MMdd`))</VersionSuffix>
<Version>$(VersionPrefix).$(VersionSuffix)</Version>
<FileVersion>$(Version)</FileVersion>
@@ -28,7 +28,7 @@
<RepositoryUrl>https://github.com/NewLifeX/AntJob</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>新生命团队;X组件;NewLife;$(AssemblyName)</PackageTags>
- <PackageReleaseNotes>修正RPC粘包处理问题</PackageReleaseNotes>
+ <PackageReleaseNotes>架构升级</PackageReleaseNotes>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
@@ -36,6 +36,10 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PackageReadmeFile>Readme.MD</PackageReadmeFile>
</PropertyGroup>
+
+ <ItemGroup>
+ <Compile Remove="Providers\HttpJobProvider.cs" />
+ </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
@@ -45,9 +49,9 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="NewLife.Core" Version="10.9.2024.402" />
- <PackageReference Include="NewLife.Remoting" Version="2.8.2024.402" />
- <PackageReference Include="NewLife.Stardust" Version="2.9.2024.402" />
+ <PackageReference Include="NewLife.Core" Version="11.0.2024.1001" />
+ <PackageReference Include="NewLife.Remoting" Version="3.1.2024.1002" />
+ <PackageReference Include="NewLife.Stardust" Version="3.1.2024.1004" />
</ItemGroup>
<ItemGroup>
diff --git a/AntJob/AntSetting.cs b/AntJob/AntSetting.cs
index 9043f31..10c7d29 100644
--- a/AntJob/AntSetting.cs
+++ b/AntJob/AntSetting.cs
@@ -2,12 +2,13 @@
using System.Reflection;
using NewLife;
using NewLife.Configuration;
+using NewLife.Remoting.Clients;
namespace AntJob;
/// <summary>蚂蚁配置。主要用于网络型调度系统</summary>
[Config("Ant")]
-public class AntSetting : Config<AntSetting>
+public class AntSetting : Config<AntSetting>, IClientSetting
{
#region 属性
/// <summary>调试开关。默认false</summary>
@@ -25,6 +26,8 @@ public class AntSetting : Config<AntSetting>
/// <summary>应用密钥。</summary>
[Description("应用密钥。")]
public String Secret { get; set; }
+
+ String IClientSetting.Code { get => AppID; set => AppID = value; }
#endregion
#region 方法
diff --git a/AntJob/Data/IJob.cs b/AntJob/Data/IJob.cs
index eff6b30..abca1bc 100644
--- a/AntJob/Data/IJob.cs
+++ b/AntJob/Data/IJob.cs
@@ -1,46 +1,46 @@
-using System;
+namespace AntJob.Data;
-namespace AntJob.Data
+/// <summary>作业参数</summary>
+public interface IJob
{
- /// <summary>作业参数</summary>
- public interface IJob
- {
- /// <summary>名称</summary>
- String Name { get; set; }
+ /// <summary>名称</summary>
+ String Name { get; set; }
- /// <summary>类名。支持该作业的处理器实现</summary>
- String ClassName { get; set; }
+ /// <summary>类名。支持该作业的处理器实现</summary>
+ String ClassName { get; set; }
- /// <summary>是否启用</summary>
- Boolean Enable { get; set; }
+ /// <summary>是否启用</summary>
+ Boolean Enable { get; set; }
- /// <summary>开始。大于等于该时间,定时作业和数据作业使用</summary>
- DateTime Start { get; set; }
+ /// <summary>数据时间。定时调度的执行时间点,或者数据调度的开始时间</summary>
+ DateTime DataTime { get; set; }
- /// <summary>结束。小于该时间,数据作业使用</summary>
- DateTime End { get; set; }
+ /// <summary>结束。小于该时间,数据作业使用</summary>
+ DateTime End { get; set; }
- /// <summary>时间偏移。距离实时时间的秒数,部分业务不能跑到实时</summary>
- Int32 Offset { get; set; }
+ /// <summary>时间偏移。距离实时时间的秒数,考虑到服务器之间的时间差,部分业务不能跑到实时</summary>
+ Int32 Offset { get; set; }
- /// <summary>步进。最大区间大小,秒</summary>
- Int32 Step { get; set; }
+ /// <summary>步进。最大区间大小,秒</summary>
+ Int32 Step { get; set; }
- /// <summary>批大小</summary>
- Int32 BatchSize { get; set; }
+ /// <summary>批大小</summary>
+ Int32 BatchSize { get; set; }
- /// <summary>并行度。最大同时执行任务数</summary>
- Int32 MaxTask { get; set; }
+ /// <summary>并行度。最大同时执行任务数</summary>
+ Int32 MaxTask { get; set; }
- /// <summary>调度模式。定时调度只要达到时间片开头就可以跑,数据调度要求达到时间片末尾才可以跑</summary>
- JobModes Mode { get; set; }
+ /// <summary>调度模式。定时调度只要达到时间片开头就可以跑,数据调度要求达到时间片末尾才可以跑</summary>
+ JobModes Mode { get; set; }
- /// <summary>消息主题</summary>
- String Topic { get; set; }
+ /// <summary>Cron定时表达式</summary>
+ String Cron { get; set; }
- /// <summary>数据</summary>
- String Data { get; set; }
- }
+ /// <summary>消息主题</summary>
+ String Topic { get; set; }
- public partial class JobModel : IJob { }
-}
\ No newline at end of file
+ /// <summary>数据</summary>
+ String Data { get; set; }
+}
+
+public partial class JobModel : IJob { }
\ No newline at end of file
diff --git a/AntJob/Data/IPeer.cs b/AntJob/Data/IPeer.cs
index 4de40a1..2d9e13c 100644
--- a/AntJob/Data/IPeer.cs
+++ b/AntJob/Data/IPeer.cs
@@ -1,28 +1,25 @@
-using System;
+namespace AntJob.Data;
-namespace AntJob.Data
+/// <summary>邻居伙伴</summary>
+public interface IPeer
{
- /// <summary>邻居伙伴</summary>
- public interface IPeer
- {
- /// <summary>实例。IP加端口,唯一</summary>
- String Instance { get; set; }
+ /// <summary>实例。IP加端口,唯一</summary>
+ String Instance { get; set; }
- /// <summary>客户端。IP加进程</summary>
- String Client { get; set; }
+ /// <summary>客户端。IP加进程</summary>
+ String Client { get; set; }
- /// <summary>名称。机器名称</summary>
- String Machine { get; set; }
+ /// <summary>名称。机器名称</summary>
+ String Machine { get; set; }
- /// <summary>版本。客户端</summary>
- String Version { get; set; }
+ /// <summary>版本。客户端</summary>
+ String Version { get; set; }
- /// <summary>创建时间</summary>
- DateTime CreateTime { get; set; }
+ /// <summary>创建时间</summary>
+ DateTime CreateTime { get; set; }
- /// <summary>更新时间</summary>
- DateTime UpdateTime { get; set; }
- }
+ /// <summary>更新时间</summary>
+ DateTime UpdateTime { get; set; }
+}
- public partial class PeerModel : IPeer { }
-}
\ No newline at end of file
+public partial class PeerModel : IPeer { }
\ No newline at end of file
diff --git a/AntJob/Data/ITask.cs b/AntJob/Data/ITask.cs
index 6e1d154..823bfc9 100644
--- a/AntJob/Data/ITask.cs
+++ b/AntJob/Data/ITask.cs
@@ -1,23 +1,20 @@
-using System;
+namespace AntJob.Data;
-namespace AntJob.Data
+/// <summary>任务参数</summary>
+public interface ITask
{
- /// <summary>任务参数</summary>
- public interface ITask
- {
- /// <summary>任务项编号</summary>
- Int32 ID { get; set; }
+ /// <summary>任务项编号</summary>
+ Int32 ID { get; set; }
- /// <summary>开始。大于等于</summary>
- DateTime Start { get; set; }
+ /// <summary>数据时间。定时调度的执行时间点,或者数据调度的开始时间</summary>
+ DateTime DataTime { get; set; }
- /// <summary>结束。小于</summary>
- DateTime End { get; set; }
+ /// <summary>结束。小于</summary>
+ DateTime End { get; set; }
- /// <summary>批大小</summary>
- Int32 BatchSize { get; set; }
+ /// <summary>批大小</summary>
+ Int32 BatchSize { get; set; }
- /// <summary>数据</summary>
- String Data { get; set; }
- }
+ /// <summary>数据</summary>
+ String Data { get; set; }
}
\ No newline at end of file
diff --git a/AntJob/Data/ITaskResult.cs b/AntJob/Data/ITaskResult.cs
index 04141dc..4929d5b 100644
--- a/AntJob/Data/ITaskResult.cs
+++ b/AntJob/Data/ITaskResult.cs
@@ -1,17 +1,14 @@
-using System;
+namespace AntJob.Data;
-namespace AntJob.Data
+/// <summary>任务结果</summary>
+public interface ITaskResult
{
- /// <summary>任务结果</summary>
- public interface ITaskResult
- {
- /// <summary>任务项编号</summary>
- Int32 ID { get; set; }
+ /// <summary>任务项编号</summary>
+ Int32 ID { get; set; }
- /// <summary>状态</summary>
- JobStatus Status { get; set; }
+ /// <summary>状态</summary>
+ JobStatus Status { get; set; }
- /// <summary>消息内容。异常信息或其它任务消息</summary>
- String Message { get; set; }
- }
+ /// <summary>消息内容。异常信息或其它任务消息</summary>
+ String Message { get; set; }
}
\ No newline at end of file
diff --git a/AntJob/Data/JobModel.cs b/AntJob/Data/JobModel.cs
index 832213c..4f85c54 100644
--- a/AntJob/Data/JobModel.cs
+++ b/AntJob/Data/JobModel.cs
@@ -1,74 +1,83 @@
-using System;
-using System.Xml.Serialization;
+using System.Xml.Serialization;
-namespace AntJob.Data
+namespace AntJob.Data;
+
+/// <summary>作业模型</summary>
+/// <remarks>定时调度只要达到时间片开头就可以跑,数据调度要求达到时间片末尾才可以跑</remarks>
+public partial class JobModel : ICloneable
{
- /// <summary>作业模型</summary>
- /// <remarks>定时调度只要达到时间片开头就可以跑,数据调度要求达到时间片末尾才可以跑</remarks>
- public partial class JobModel
- {
- #region 属性
- /// <summary>名称</summary>
- [XmlAttribute]
- public String Name { get; set; }
-
- /// <summary>类名。支持该作业的处理器实现</summary>
- [XmlAttribute]
- public String ClassName { get; set; }
-
- /// <summary>是否启用</summary>
- [XmlAttribute]
- public Boolean Enable { get; set; }
-
- /// <summary>开始。大于等于</summary>
- [XmlAttribute]
- public DateTime Start { get; set; }
-
- /// <summary>结束。小于</summary>
- [XmlAttribute]
- public DateTime End { get; set; }
-
- /// <summary>时间偏移。距离实时时间的秒数,部分业务不能跑到实时</summary>
- [XmlAttribute]
- public Int32 Offset { get; set; }
-
- /// <summary>步进。最大区间大小,秒</summary>
- [XmlAttribute]
- public Int32 Step { get; set; }
-
- /// <summary>批大小</summary>
- [XmlAttribute]
- public Int32 BatchSize { get; set; } = 5000;
-
- /// <summary>最大任务数</summary>
- [XmlAttribute]
- public Int32 MaxTask { get; set; }
-
- /// <summary>调度模式。定时调度只要达到时间片开头就可以跑,数据调度要求达到时间片末尾才可以跑</summary>
- [XmlAttribute]
- public JobModes Mode { get; set; }
-
- /// <summary>显示名</summary>
- [XmlAttribute]
- public String DisplayName { get; set; }
-
- /// <summary>描述</summary>
- [XmlAttribute]
- public String Description { get; set; }
-
- /// <summary>消息主题</summary>
- [XmlAttribute]
- public String Topic { get; set; }
-
- /// <summary>数据</summary>
- [XmlAttribute]
- public String Data { get; set; }
- #endregion
-
- #region 构造
- /// <summary>已重载。</summary>
- /// <returns></returns>
- public override String ToString() => Name;
- #endregion
- }
+ #region 属性
+ /// <summary>名称</summary>
+ [XmlAttribute]
+ public String Name { get; set; }
+
+ /// <summary>类名。支持该作业的处理器实现</summary>
+ [XmlAttribute]
+ public String ClassName { get; set; }
+
+ /// <summary>是否启用</summary>
+ [XmlAttribute]
+ public Boolean Enable { get; set; }
+
+ /// <summary>数据时间。定时调度的执行时间点,或者数据调度的开始时间</summary>
+ [XmlAttribute]
+ public DateTime DataTime { get; set; }
+
+ /// <summary>开始时间。兼容旧版</summary>
+ [XmlAttribute]
+ [Obsolete]
+ public DateTime Start { get; set; }
+
+ /// <summary>结束。小于</summary>
+ [XmlAttribute]
+ public DateTime End { get; set; }
+
+ /// <summary>时间偏移。距离实时时间的秒数,考虑到服务器之间的时间差,部分业务不能跑到实时</summary>
+ [XmlAttribute]
+ public Int32 Offset { get; set; }
+
+ /// <summary>步进。最大区间大小,秒</summary>
+ [XmlAttribute]
+ public Int32 Step { get; set; }
+
+ /// <summary>批大小</summary>
+ [XmlAttribute]
+ public Int32 BatchSize { get; set; } = 5000;
+
+ /// <summary>最大任务数</summary>
+ [XmlAttribute]
+ public Int32 MaxTask { get; set; }
+
+ /// <summary>调度模式。定时调度只要达到时间片开头就可以跑,数据调度要求达到时间片末尾才可以跑</summary>
+ [XmlAttribute]
+ public JobModes Mode { get; set; }
+
+ /// <summary>显示名</summary>
+ [XmlAttribute]
+ public String DisplayName { get; set; }
+
+ /// <summary>描述</summary>
+ [XmlAttribute]
+ public String Description { get; set; }
+
+ /// <summary>Cron定时表达式</summary>
+ [XmlAttribute]
+ public String Cron { get; set; }
+
+ /// <summary>消息主题</summary>
+ [XmlAttribute]
+ public String Topic { get; set; }
+
+ /// <summary>数据</summary>
+ [XmlAttribute]
+ public String Data { get; set; }
+ #endregion
+
+ #region 构造
+ /// <summary>已重载。</summary>
+ /// <returns></returns>
+ public override String ToString() => Name;
+
+ Object ICloneable.Clone() => MemberwiseClone();
+ #endregion
}
\ No newline at end of file
diff --git a/AntJob/Data/JobModes.cs b/AntJob/Data/JobModes.cs
index 06e1296..52730b2 100644
--- a/AntJob/Data/JobModes.cs
+++ b/AntJob/Data/JobModes.cs
@@ -1,30 +1,29 @@
using System.ComponentModel;
-namespace AntJob.Data
+namespace AntJob.Data;
+
+/// <summary>作业模式</summary>
+/// <remarks>定时调度只要达到时间片开头就可以跑,数据调度要求达到时间片末尾才可以跑</remarks>
+//[Description("作业模式")]
+public enum JobModes
{
- /// <summary>作业模式</summary>
- /// <remarks>定时调度只要达到时间片开头就可以跑,数据调度要求达到时间片末尾才可以跑</remarks>
- //[Description("作业模式")]
- public enum JobModes
- {
- /// <summary>数据调度</summary>
- [Description("数据调度")]
- Data = 1,
+ /// <summary>数据调度</summary>
+ [Description("数据调度")]
+ Data = 1,
- /// <summary>定时调度</summary>
- [Description("定时调度")]
- Alarm = 2,
+ /// <summary>定时调度</summary>
+ [Description("定时调度")]
+ Time = 2,
- /// <summary>消息调度</summary>
- [Description("消息调度")]
- Message = 3,
+ /// <summary>消息调度</summary>
+ [Description("消息调度")]
+ Message = 3,
- ///// <summary>C#调度</summary>
- //[Description("C#调度")]
- //CSharp = 4,
+ ///// <summary>C#调度</summary>
+ //[Description("C#调度")]
+ //CSharp = 4,
- ///// <summary>SQL调度</summary>
- //[Description("SQL调度")]
- //Sql = 5,
- }
+ ///// <summary>SQL调度</summary>
+ //[Description("SQL调度")]
+ //Sql = 5,
}
\ No newline at end of file
diff --git a/AntJob/Data/JobStatus.cs b/AntJob/Data/JobStatus.cs
index d23d36b..06b51a9 100644
--- a/AntJob/Data/JobStatus.cs
+++ b/AntJob/Data/JobStatus.cs
@@ -1,24 +1,26 @@
-namespace AntJob.Data
+namespace AntJob.Data;
+
+/// <summary>作业状态</summary>
+public enum JobStatus
{
- /// <summary>作业状态</summary>
- public enum JobStatus
- {
- /// <summary>就绪</summary>
- 就绪 = 0,
+ /// <summary>就绪</summary>
+ 就绪 = 0,
+
+ /// <summary>抽取中</summary>
+ 抽取中 = 1,
- /// <summary>抽取中</summary>
- 抽取中 = 1,
+ /// <summary>处理中</summary>
+ 处理中 = 2,
- /// <summary>处理中</summary>
- 处理中 = 2,
+ /// <summary>错误</summary>
+ 错误 = 3,
- /// <summary>错误</summary>
- 错误 = 3,
+ /// <summary>已完成</summary>
+ 完成 = 4,
- /// <summary>已完成</summary>
- 完成 = 4,
+ /// <summary>已取消</summary>
+ 取消 = 5,
- /// <summary>已取消</summary>
- 取消 = 5,
- }
+ /// <summary>延迟重试</summary>
+ 延迟 = 6,
}
\ No newline at end of file
diff --git a/AntJob/Data/PeerModel.cs b/AntJob/Data/PeerModel.cs
index 8bfe0a5..945fdff 100644
--- a/AntJob/Data/PeerModel.cs
+++ b/AntJob/Data/PeerModel.cs
@@ -1,26 +1,23 @@
-using System;
+namespace AntJob.Data;
-namespace AntJob.Data
+/// <summary>邻居伙伴</summary>
+public partial class PeerModel
{
- /// <summary>邻居伙伴</summary>
- public partial class PeerModel
- {
- /// <summary>实例。IP加端口</summary>
- public String Instance { get; set; }
+ /// <summary>实例。IP加端口</summary>
+ public String Instance { get; set; }
- /// <summary>客户端。IP加进程</summary>
- public String Client { get; set; }
+ /// <summary>客户端。IP加进程</summary>
+ public String Client { get; set; }
- /// <summary>名称。机器名称</summary>
- public String Machine { get; set; }
+ /// <summary>名称。机器名称</summary>
+ public String Machine { get; set; }
- /// <summary>版本。客户端</summary>
- public String Version { get; set; }
+ /// <summary>版本。客户端</summary>
+ public String Version { get; set; }
- /// <summary>创建时间</summary>
- public DateTime CreateTime { get; set; }
+ /// <summary>创建时间</summary>
+ public DateTime CreateTime { get; set; }
- /// <summary>更新时间</summary>
- public DateTime UpdateTime { get; set; }
- }
+ /// <summary>更新时间</summary>
+ public DateTime UpdateTime { get; set; }
}
\ No newline at end of file
diff --git a/AntJob/Data/TaskModel.cs b/AntJob/Data/TaskModel.cs
index 1c960d7..93ee818 100644
--- a/AntJob/Data/TaskModel.cs
+++ b/AntJob/Data/TaskModel.cs
@@ -1,25 +1,26 @@
-using System;
+namespace AntJob.Data;
-namespace AntJob.Data
+/// <summary>任务模型</summary>
+public partial class TaskModel : ITask
{
- /// <summary>任务模型</summary>
- public partial class TaskModel : ITask
- {
- #region 属性
- /// <summary>编号</summary>
- public Int32 ID { get; set; }
+ #region 属性
+ /// <summary>编号</summary>
+ public Int32 ID { get; set; }
- /// <summary>开始。大于等于</summary>
- public DateTime Start { get; set; }
+ /// <summary>数据时间。定时调度的执行时间点,或者数据调度的开始时间</summary>
+ public DateTime DataTime { get; set; }
- /// <summary>结束。小于,不等于</summary>
- public DateTime End { get; set; }
+ /// <summary>开始时间。兼容旧版</summary>
+ [Obsolete]
+ public DateTime Start { get; set; }
- /// <summary>批大小</summary>
- public Int32 BatchSize { get; set; }
+ /// <summary>结束。小于,不等于</summary>
+ public DateTime End { get; set; }
- /// <summary>数据</summary>
- public String Data { get; set; }
- #endregion
- }
+ /// <summary>批大小</summary>
+ public Int32 BatchSize { get; set; }
+
+ /// <summary>数据</summary>
+ public String Data { get; set; }
+ #endregion
}
\ No newline at end of file
diff --git a/AntJob/Data/TaskResult.cs b/AntJob/Data/TaskResult.cs
index 3cc32b6..b0c2fd7 100644
--- a/AntJob/Data/TaskResult.cs
+++ b/AntJob/Data/TaskResult.cs
@@ -1,40 +1,43 @@
-using System;
+namespace AntJob.Data;
-namespace AntJob.Data
+/// <summary>任务结果</summary>
+public partial class TaskResult : ITaskResult
{
- /// <summary>任务结果</summary>
- public partial class TaskResult : ITaskResult
- {
- #region 属性
- /// <summary>编号</summary>
- public Int32 ID { get; set; }
+ #region 属性
+ /// <summary>编号</summary>
+ public Int32 ID { get; set; }
- /// <summary>总数</summary>
- public Int32 Total { get; set; }
+ /// <summary>总数</summary>
+ public Int32 Total { get; set; }
- /// <summary>成功</summary>
- public Int32 Success { get; set; }
+ /// <summary>成功</summary>
+ public Int32 Success { get; set; }
- /// <summary>耗时,秒</summary>
- public Int32 Cost { get; set; }
+ /// <summary>耗时,毫秒</summary>
+ public Int32 Cost { get; set; }
- /// <summary>错误</summary>
- public Int32 Error { get; set; }
+ /// <summary>错误</summary>
+ public Int32 Error { get; set; }
- /// <summary>次数</summary>
- public Int32 Times { get; set; }
+ /// <summary>次数</summary>
+ public Int32 Times { get; set; }
- /// <summary>速度</summary>
- public Int32 Speed { get; set; }
+ /// <summary>速度。每秒处理数据量</summary>
+ public Int32 Speed { get; set; }
- /// <summary>状态</summary>
- public JobStatus Status { get; set; }
+ /// <summary>状态</summary>
+ public JobStatus Status { get; set; }
- /// <summary>最后键值</summary>
- public String Key { get; set; }
+ /// <summary>下一次执行时间。UTC时间,确保时区兼容</summary>
+ public DateTime NextTime { get; set; }
- /// <summary>消息内容。异常信息或其它任务消息</summary>
- public String Message { get; set; }
- #endregion
- }
+ /// <summary>最后键值</summary>
+ public String Key { get; set; }
+
+ /// <summary>链路追踪</summary>
+ public String TraceId { get; set; }
+
+ /// <summary>消息内容。异常信息或其它任务消息</summary>
+ public String Message { get; set; }
+ #endregion
}
\ No newline at end of file
diff --git a/AntJob/Data/TemplateHelper.cs b/AntJob/Data/TemplateHelper.cs
index 4de2487..c373e9f 100644
--- a/AntJob/Data/TemplateHelper.cs
+++ b/AntJob/Data/TemplateHelper.cs
@@ -20,20 +20,21 @@ public static class TemplateHelper
var p = 0;
while (true)
{
- var ti = Find(str, "Start", p);
- if (ti == null)
+ var ti = Find(str, "DataTime", p);
+ if (ti.IsEmpty) ti = Find(str, "dt", p);
+ if (ti.IsEmpty)
{
sb.Append(str.Substring(p));
break;
}
// 准备替换
- var val = ti.Item3.IsNullOrEmpty() ? startTime.ToFullString() : startTime.ToString(ti.Item3);
- sb.Append(str.Substring(p, ti.Item1 - p));
+ var val = ti.Format.IsNullOrEmpty() ? startTime.ToFullString() : startTime.ToString(ti.Format);
+ sb.Append(str.Substring(p, ti.Start - p));
sb.Append(val);
// 移动指针
- p = ti.Item2 + 1;
+ p = ti.End + 1;
}
str = sb.ToString();
@@ -42,32 +43,32 @@ public static class TemplateHelper
while (true)
{
var ti = Find(str, "End", p);
- if (ti == null)
+ if (ti.IsEmpty)
{
sb.Append(str.Substring(p));
break;
}
// 准备替换
- var val = ti.Item3.IsNullOrEmpty() ? endTime.ToFullString() : endTime.ToString(ti.Item3);
- sb.Append(str.Substring(p, ti.Item1 - p));
+ var val = ti.Format.IsNullOrEmpty() ? endTime.ToFullString() : endTime.ToString(ti.Format);
+ sb.Append(str.Substring(p, ti.Start - p));
sb.Append(val);
// 移动指针
- p = ti.Item2 + 1;
+ p = ti.End + 1;
}
return sb.Put(true);
}
- private static Tuple<Int32, Int32, String> Find(String str, String key, Int32 p)
+ private static VarItem Find(String str, String key, Int32 p)
{
// 头尾
var p1 = str.IndexOf("{" + key, p);
- if (p1 < 0) return null;
+ if (p1 < 0) return _empty;
var p2 = str.IndexOf("}", p1);
- if (p2 < 0) return null;
+ if (p2 < 0) return _empty;
// 格式化字符串
var format = "";
@@ -75,7 +76,17 @@ public static class TemplateHelper
if (p3 > 0 && p3 < p2) format = str.Substring(p3 + 1, p2 - p3 - 1);
// 左括号位置,右括号位置,格式化字符串
- return new Tuple<Int32, Int32, String>(p1, p2, format);
+ return new VarItem(p1, p2, format);
+ }
+
+ private static VarItem _empty = new(-1, -1, "");
+ struct VarItem(Int32 start, Int32 end, String format)
+ {
+ public Int32 Start = start;
+ public Int32 End = end;
+ public String Format = format;
+
+ public readonly Boolean IsEmpty => Start < 0;
}
/// <summary>使用消息数组处理模板</summary>
diff --git a/AntJob/Data/TimeExpression.cs b/AntJob/Data/TimeExpression.cs
new file mode 100644
index 0000000..41b334a
--- /dev/null
+++ b/AntJob/Data/TimeExpression.cs
@@ -0,0 +1,170 @@
+using NewLife;
+
+namespace AntJob.Data;
+
+/// <summary>时间表达式,一次解析多次使用。如{dt+1M+5d:yyyyMMdd}</summary>
+public class TimeExpression
+{
+ #region 属性
+ /// <summary>表达式</summary>
+ public String Expression { get; set; }
+
+ /// <summary>变量名</summary>
+ public String VarName { get; set; } = "dt";
+
+ /// <summary>格式化字符串</summary>
+ public String Format { get; set; }
+
+ /// <summary>时间表达式项集合</summary>
+ public IList<TimeExpressionItem> Items { get; } = [];
+ #endregion
+
+ #region 构造
+ /// <summary>实例化时间表达式</summary>
+ public TimeExpression() { }
+
+ /// <summary>实例化时间表达式</summary>
+ public TimeExpression(String expression) => Parse(expression);
+ #endregion
+
+ #region 方法
+ /// <summary>解析表达式</summary>
+ public Boolean Parse(String expression)
+ {
+ var p1 = expression.IndexOf('{');
+ if (p1 < 0) return false;
+
+ var p2 = expression.IndexOf('}', p1);
+ if (p2 < 0) return false;
+
+ expression = expression.Substring(p1 + 1, p2 - p1 - 1);
+
+ // 循环查找
+ var ms = Items;
+ p1 = -1;
+ while (true)
+ {
+ p2 = expression.IndexOfAny(['+', '-', ':', ','], p1 + 1);
+ if (p2 < 0) p2 = expression.Length;
+
+ // 第一段是变量名
+ if (p1 < 0 && p2 > 0)
+ {
+ VarName = expression[0..p2];
+ }
+ else if (expression[p1] is '+' or '-')
+ {
+ var str = expression[p1..p2];
+ var item = new TimeExpressionItem();
+ if (!item.Parse(str)) throw new InvalidDataException($"Invalid [{str}]");
+
+ ms.Add(item);
+ }
+ else if (expression[p1] is ':' or ',')
+ {
+ // 最后一段是格式化字符串
+ p2 = expression.Length;
+ Format = expression[(p1 + 1)..p2];
+ }
+
+ if (p2 >= expression.Length) break;
+ p1 = p2;
+ }
+
+ // 默认天级
+ if (ms.Count == 0) ms.Add(new TimeExpressionItem { Level = "d", Value = 0 });
+
+ Expression = expression;
+
+ return true;
+ }
+
+ /// <summary>执行时间偏移</summary>
+ public DateTime Execute(DateTime time)
+ {
+ foreach (var item in Items)
+ {
+ time = item.Execute(time);
+ }
+
+ return time;
+ }
+
+ /// <summary>构建时间字符串</summary>
+ public String Build(DateTime time)
+ {
+ time = Execute(time);
+
+ var ms = Items;
+ var format = Format;
+ if (format.IsNullOrEmpty() && ms.Count > 0) format = ms[ms.Count - 1].GetFormat();
+ if (format.IsNullOrEmpty()) format = "yyyyMMdd";
+
+ return time.ToString(format);
+ }
+ #endregion
+
+ /// <summary>处理时间偏移模版。如{dt+1M+5d:yyyyMMdd}</summary>
+ /// <param name="template"></param>
+ /// <param name="time"></param>
+ /// <returns></returns>
+ public static String Build(String template, DateTime time)
+ {
+ return null;
+ }
+}
+
+/// <summary>时间表达式项。如+5d</summary>
+public class TimeExpressionItem
+{
+ /// <summary>级别。如y/M/d/H/m/w</summary>
+ public String Level { get; set; }
+
+ /// <summary>数值。包括正负</summary>
+ public Int32 Value { get; set; }
+
+ /// <summary>分解表达式项。如+5d</summary>
+ /// <param name="value"></param>
+ /// <returns></returns>
+ public Boolean Parse(String value)
+ {
+ if (value.IsNullOrEmpty() || value.Length < 3) return false;
+
+ Level = value[^1..];
+ Value = value[..^1].TrimStart('+').ToInt();
+
+ return true;
+ }
+
+ /// <summary>执行时间偏移</summary>
+ public DateTime Execute(DateTime time)
+ {
+ return Level switch
+ {
+ "y" => new DateTime(time.Year, 1, 1, 0, 0, 0, time.Kind).AddYears(Value),
+ "M" => new DateTime(time.Year, time.Month, 1, 0, 0, 0, time.Kind).AddMonths(Value),
+ "d" => new DateTime(time.Year, time.Month, time.Day, 0, 0, 0, time.Kind).AddDays(Value),
+ "H" => new DateTime(time.Year, time.Month, time.Day, time.Hour, 0, 0, time.Kind).AddHours(Value),
+ "m" => new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, 0, time.Kind).AddMinutes(Value),
+ "s" => new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second, time.Kind).AddSeconds(Value),
+ "w" => new DateTime(time.Year, time.Month, time.Day, 0, 0, 0, time.Kind).AddDays(Value * 7),
+ _ => time,
+ };
+ }
+
+ /// <summary>获取格式化字符串</summary>
+ public String GetFormat()
+ {
+ return Level switch
+ {
+ "y" => "yyyy",
+ "M" => "yyyyMM",
+ "d" => "yyyyMMdd",
+ "H" => "yyyyMMddHH",
+ "m" => "yyyyMMddHHmm",
+ "s" => "yyyyMMddHHmmss",
+ "w" => "yyyyww",
+ _ => "",
+ };
+ }
+}
\ No newline at end of file
diff --git a/AntJob/Handler.cs b/AntJob/Handler.cs
index 253c5b5..2e146a5 100644
--- a/AntJob/Handler.cs
+++ b/AntJob/Handler.cs
@@ -2,19 +2,25 @@
using AntJob.Data;
using AntJob.Providers;
using NewLife;
-using NewLife.Collections;
+using NewLife.Data;
using NewLife.Log;
namespace AntJob;
/// <summary>处理器基类,每个作业一个处理器</summary>
/// <remarks>
+/// 文档:https://newlifex.com/blood/antjob
+///
/// 每个作业一个处理器类,负责一个业务处理模块。
/// 例如在数据同步或数据清洗中,每张表就写一个处理器,如果一组数据表有共同特性,还可以为它们封装一个自己的处理器基类。
///
/// 定时调度只要当前时间达到时间片开头就可以跑,数据调度要求达到时间片末尾才可以跑。
+///
+/// 调度器控制方法:Start|Stop|Acquire
+/// 任务处理流程:Process->OnProcess->Execute->OnFinish
+/// 任务控制方法:Produce|Delay
/// </remarks>
-public abstract class Handler
+public abstract class Handler : IExtend, ITracerFeature, ILogFeature
{
#region 属性
/// <summary>名称</summary>
@@ -33,22 +39,24 @@ public abstract class Handler
public Boolean Active { get; private set; }
/// <summary>调度模式</summary>
- public virtual JobModes Mode { get; set; } = JobModes.Alarm;
+ public virtual JobModes Mode { get; set; } = JobModes.Time;
private volatile Int32 _Busy;
/// <summary>正在处理中的任务数</summary>
public Int32 Busy => _Busy;
- /// <summary>性能跟踪器</summary>
- public ITracer Tracer { get; set; }
+ private Int32 _speed;
#endregion
#region 索引器
- private readonly IDictionary<String, Object> _Items = new NullableDictionary<String, Object>(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary<String, Object> _Items = [];
+ /// <summary>扩展数据</summary>
+ IDictionary<String, Object> IExtend.Items => _Items;
+
/// <summary>用户数据</summary>
/// <param name="item"></param>
/// <returns></returns>
- public Object this[String item] { get => _Items[item]; set => _Items[item] = value; }
+ public Object this[String item] { get => _Items.TryGetValue(item, out var obj) ? obj : null; set => _Items[item] = value; }
#endregion
#region 构造
@@ -61,10 +69,11 @@ public abstract class Handler
var now = DateTime.Now;
var job = new JobModel
{
- Start = new DateTime(now.Year, now.Month, 1),
+ DataTime = now.Date,
Step = 30,
Offset = 15,
- Mode = JobModes.Alarm,
+ Mode = JobModes.Time,
+ Cron = "0/30 * * *",
};
// 默认并发数为核心数
@@ -76,6 +85,9 @@ public abstract class Handler
#endregion
#region 基本方法
+ /// <summary>初始化。作业处理器启动之前</summary>
+ public virtual void Init() { }
+
/// <summary>开始</summary>
public virtual Boolean Start()
{
@@ -83,7 +95,7 @@ public abstract class Handler
var msg = "开始工作";
var job = Job;
- if (job != null) msg += $" {job.Enable} 区间({job.Start.ToFullString("")}, {job.End.ToFullString("")}) Offset={job.Offset} Step={job.Step} MaxTask={job.MaxTask}";
+ if (job != null) msg += $" {job.Enable} 区间({job.DataTime.ToFullString("")}, {job.End.ToFullString("")}) Offset={job.Offset} Step={job.Step} MaxTask={job.MaxTask}";
using var span = Tracer?.NewSpan($"job:{Name}:Start", msg);
WriteLog(msg);
@@ -131,25 +143,31 @@ public abstract class Handler
/// <summary>处理一项新任务</summary>
/// <param name="task"></param>
- public void Process(ITask task)
+ public virtual void Process(ITask task)
{
if (task == null) return;
+ var result = new TaskResult { ID = task.ID };
var ctx = new JobContext
{
Handler = this,
Task = task,
- Result = new TaskResult { ID = task.ID },
+ Result = result,
};
// APM埋点
- var span = Schedule.Tracer?.NewSpan($"job:{Name}", task.Data ?? $"({task.Start.ToFullString()}, {task.End.ToFullString()})");
- ctx.Remark = span?.ToString();
+ using var span = Schedule?.Tracer?.NewSpan($"job:{Name}", task.Data ?? $"({task.DataTime.ToFullString()}, {task.End.ToFullString()})");
+ result.TraceId = span?.TraceId;
+
+ // 较慢的作业,及时报告进度
+ if (_speed < 10) Report(ctx, JobStatus.处理中);
var sw = Stopwatch.StartNew();
try
{
OnProcess(ctx);
+
+ if (span != null) span.Value = ctx.Total;
}
catch (Exception ex)
{
@@ -161,16 +179,17 @@ public abstract class Handler
finally
{
Interlocked.Decrement(ref _Busy);
- span?.Dispose();
+ //span?.Dispose();
}
sw.Stop();
ctx.Cost = sw.Elapsed.TotalMilliseconds;
+ _speed = ctx.Speed;
OnFinish(ctx);
Schedule?.OnFinish(ctx);
- ctx.Items.Clear();
+ //ctx.Items.Clear();
}
/// <summary>处理任务。内部分批处理</summary>
@@ -180,27 +199,48 @@ public abstract class Handler
ctx.Total = 1;
ctx.Success = Execute(ctx);
}
+
+ /// <summary>报告任务状态</summary>
+ /// <param name="ctx"></param>
+ /// <param name="status"></param>
+ protected virtual void Report(JobContext ctx, JobStatus status)
+ {
+ ctx.Status = status;
+ Provider?.Report(ctx);
+ }
+
+ /// <summary>整个任务完成</summary>
+ /// <param name="ctx"></param>
+ protected virtual void OnFinish(JobContext ctx) => Provider?.Finish(ctx);
#endregion
#region 数据处理
/// <summary>处理一批数据,一个任务内多次调用</summary>
/// <param name="ctx">上下文</param>
/// <returns></returns>
- protected abstract Int32 Execute(JobContext ctx);
+ public abstract Int32 Execute(JobContext ctx);
/// <summary>生产消息</summary>
/// <param name="topic">主题</param>
/// <param name="messages">消息集合</param>
/// <param name="option">消息选项</param>
/// <returns></returns>
- public Int32 Produce(String topic, String[] messages, MessageOption option = null) => Provider.Produce(Job?.Name, topic, messages, option);
+ public virtual Int32 Produce(String topic, String[] messages, MessageOption option = null) => Provider.Produce(Job?.Name, topic, messages, option);
- /// <summary>整个任务完成</summary>
+ /// <summary>延迟执行,指定下一次执行时间</summary>
/// <param name="ctx"></param>
- protected virtual void OnFinish(JobContext ctx) => Provider?.Finish(ctx);
+ /// <param name="nextTime"></param>
+ public virtual void Delay(JobContext ctx, DateTime nextTime)
+ {
+ ctx.Status = JobStatus.延迟;
+ ctx.NextTime = nextTime;
+ }
#endregion
#region 日志
+ /// <summary>性能跟踪器</summary>
+ public ITracer Tracer { get; set; }
+
/// <summary>日志</summary>
public ILog Log { get; set; } = Logger.Null;
diff --git a/AntJob/Handlers/CSharpHandler.cs b/AntJob/Handlers/CSharpHandler.cs
index c34bfd0..6e74175 100644
--- a/AntJob/Handlers/CSharpHandler.cs
+++ b/AntJob/Handlers/CSharpHandler.cs
@@ -1,4 +1,5 @@
-using NewLife;
+using AntJob.Data;
+using NewLife;
namespace AntJob.Handlers;
@@ -15,7 +16,7 @@ public class CSharpHandler : Handler
/// <summary>实例化</summary>
public CSharpHandler()
{
- //Mode = JobModes.CSharp;
+ Mode = JobModes.Time;
var job = Job;
job.BatchSize = 8;
@@ -25,7 +26,7 @@ public class CSharpHandler : Handler
/// <summary>执行</summary>
/// <param name="ctx"></param>
/// <returns></returns>
- protected override Int32 Execute(JobContext ctx)
+ public override Int32 Execute(JobContext ctx)
{
var code = ctx.Data as String;
if (code.IsNullOrWhiteSpace()) return -1;
diff --git a/AntJob/Handlers/MessageHandler.cs b/AntJob/Handlers/MessageHandler.cs
index 0d3edd0..901fc7f 100644
--- a/AntJob/Handlers/MessageHandler.cs
+++ b/AntJob/Handlers/MessageHandler.cs
@@ -1,6 +1,7 @@
using System.Collections;
using AntJob.Data;
using NewLife;
+using NewLife.Log;
using NewLife.Serialization;
namespace AntJob.Handlers;
@@ -58,18 +59,11 @@ public abstract class MessageHandler : Handler
var ss = ctx.Task.Data.ToJsonEntity<String[]>();
if (ss == null || ss.Length == 0) return;
- //// 消息作业特殊优待字符串,不需要再次Json解码
- //if (typeof(TModel) == typeof(String))
- //{
ctx.Total = ss.Length;
ctx.Data = ss;
- //}
- //else
- //{
- // var ms = ss.Select(e => e.ToJsonEntity<TModel>()).ToList();
- // ctx.Total = ms.Count;
- // ctx.Data = ms;
- //}
+
+ var span = DefaultSpan.Current;
+ if (span != null) span.Value = ctx.Total;
Execute(ctx);
}
@@ -77,14 +71,11 @@ public abstract class MessageHandler : Handler
/// <summary>根据解码后的消息执行任务</summary>
/// <param name="ctx">上下文</param>
/// <returns></returns>
- protected override Int32 Execute(JobContext ctx)
+ public override Int32 Execute(JobContext ctx)
{
var count = 0;
foreach (String item in ctx.Data as IEnumerable)
{
- //ctx.Key = item as String;
- //ctx.Entity = item;
-
if (ProcessItem(ctx, item)) count++;
}
@@ -95,6 +86,6 @@ public abstract class MessageHandler : Handler
/// <param name="ctx">上下文</param>
/// <param name="message">消息</param>
/// <returns></returns>
- protected virtual Boolean ProcessItem(JobContext ctx, String message) => true;
+ public virtual Boolean ProcessItem(JobContext ctx, String message) => true;
#endregion
}
\ No newline at end of file
diff --git a/AntJob/JobContext.cs b/AntJob/JobContext.cs
index d9f3c7b..a5b27b1 100644
--- a/AntJob/JobContext.cs
+++ b/AntJob/JobContext.cs
@@ -1,4 +1,5 @@
-using AntJob.Data;
+using System.Collections;
+using AntJob.Data;
using NewLife.Collections;
using NewLife.Data;
@@ -35,12 +36,12 @@ public class JobContext : IExtend
/// <summary>最后处理键值。由业务决定,便于分析问题</summary>
public String Key { get; set; }
- ///// <summary>当前处理对象</summary>
- //public Object Entity { get; set; }
-
/// <summary>处理异常</summary>
public Exception Error { get; set; }
+ /// <summary>下一次执行时间</summary>
+ public DateTime NextTime { get; set; }
+
/// <summary>任务备注消息。可用于保存到任务项内容字段</summary>
public String Remark { get; set; }
#endregion
@@ -59,4 +60,20 @@ public class JobContext : IExtend
/// <summary>处理速度</summary>
public Int32 Speed => (Cost <= 0 || Total == 0) ? 0 : (Int32)Math.Min(Total * 1000L / Cost, Int32.MaxValue);
#endregion
+
+ #region 方法
+ /// <summary>根据指定实体类型返回数据列表</summary>
+ /// <typeparam name="T"></typeparam>
+ /// <returns></returns>
+ public IList<T> GetDatas<T>()
+ {
+ if (Data == null) return null;
+ if (Data is IList<T> data) return data;
+
+ // 修改列表类型,由 IList<IEntity> 改为 IList<TEntity> ,方便用户使用
+ if (Data is IEnumerable enumerable) return enumerable.Cast<T>().ToList();
+
+ return null;
+ }
+ #endregion
}
\ No newline at end of file
diff --git a/AntJob/Models/LoginModel.cs b/AntJob/Models/LoginModel.cs
index 9115867..c9c2bbe 100644
--- a/AntJob/Models/LoginModel.cs
+++ b/AntJob/Models/LoginModel.cs
@@ -1,7 +1,9 @@
-namespace AntJob.Models;
+using NewLife.Remoting.Models;
+
+namespace AntJob.Models;
/// <summary>登录模型</summary>
-public class LoginModel
+public class LoginModel : LoginRequest
{
/// <summary>用户名</summary>
public String User { get; set; }
@@ -18,22 +20,22 @@ public class LoginModel
/// <summary>进程Id</summary>
public Int32 ProcessId { get; set; }
- /// <summary>版本</summary>
- public String Version { get; set; }
+ ///// <summary>版本</summary>
+ //public String Version { get; set; }
- /// <summary>编译时间</summary>
- public DateTime Compile { get; set; }
+ ///// <summary>编译时间</summary>
+ //public DateTime Compile { get; set; }
}
-/// <summary>登录响应</summary>
-public class LoginResponse
-{
- /// <summary>名称</summary>
- public String Name { get; set; }
+///// <summary>登录响应</summary>
+//public class LoginResponse
+//{
+// /// <summary>名称</summary>
+// public String Name { get; set; }
- /// <summary>密钥。仅注册时返回</summary>
- public String Secret { get; set; }
+// /// <summary>密钥。仅注册时返回</summary>
+// public String Secret { get; set; }
- /// <summary>显示名</summary>
- public String DisplayName { get; set; }
-}
\ No newline at end of file
+// /// <summary>显示名</summary>
+// public String DisplayName { get; set; }
+//}
\ No newline at end of file
diff --git a/AntJob/Providers/AntClient.cs b/AntJob/Providers/AntClient.cs
index 446a3da..163971f 100644
--- a/AntJob/Providers/AntClient.cs
+++ b/AntJob/Providers/AntClient.cs
@@ -4,71 +4,75 @@ using System.Reflection;
using AntJob.Data;
using AntJob.Models;
using NewLife;
-using NewLife.Log;
-using NewLife.Net;
+using NewLife.Model;
using NewLife.Reflection;
-using NewLife.Remoting;
-using NewLife.Serialization;
+using NewLife.Remoting.Clients;
+using NewLife.Remoting.Models;
namespace AntJob.Providers;
/// <summary>蚂蚁客户端</summary>
-public class AntClient : ApiClient
+public class AntClient : ClientBase
{
#region 属性
- /// <summary>用户名</summary>
- public String UserName { get; set; }
-
- /// <summary>密码</summary>
- public String Password { get; set; }
-
- /// <summary>是否已登录</summary>
- public Boolean Logined { get; set; }
-
- /// <summary>最后一次登录成功后的消息</summary>
- public LoginResponse Info { get; private set; }
+ private readonly AntSetting _setting;
#endregion
- #region 方法
+ #region 构造
+ /// <summary>实例化</summary>
+ public AntClient() => InitClient();
+
/// <summary>实例化</summary>
- public AntClient()
+ /// <param name="setting"></param>
+ public AntClient(AntSetting setting) : base(setting)
{
- Log = XTrace.Log;
+ _setting = setting;
- //StatPeriod = 60;
- ShowError = true;
+ InitClient();
+ }
+
+ private void InitClient()
+ {
+ Features = Features.Login | Features.Ping;
-#if DEBUG
- EncoderLog = XTrace.Log;
- StatPeriod = 10;
-#endif
+ if (Server.StartsWithIgnoreCase("http://", "https://"))
+ SetActions("AntJob/");
+ else
+ SetActions("");
}
+ #endregion
- /// <summary>实例化</summary>
- /// <param name="uri"></param>
- public AntClient(String uri) : this()
+ #region 方法
+ /// <summary>初始化</summary>
+ protected override void OnInit()
{
- if (!uri.IsNullOrEmpty())
+ var provider = ServiceProvider ??= ObjectContainer.Provider;
+
+ // 找到容器,注册默认的模型实现,供后续InvokeAsync时自动创建正确的模型对象
+ var container = ModelExtension.GetService<IObjectContainer>(provider) ?? ObjectContainer.Current;
+ if (container != null)
{
- var ss = uri.Split(",", ";");
+ container.TryAddTransient<ILoginRequest, LoginModel>();
+ //container.TryAddTransient<ILoginResponse, LoginResponse>();
+ //container.TryAddTransient<ILogoutResponse, LogoutResponse>();
+ //container.TryAddTransient<IPingRequest, PingInfo>();
+ //container.TryAddTransient<IPingResponse, PingResponse>();
+ //container.TryAddTransient<IUpgradeInfo, UpgradeInfo>();
+ }
- Servers = ss;
+ InitClient();
- var u = new Uri(ss[0]);
- var us = u.UserInfo.Split(":");
- if (us.Length > 0) UserName = us[0];
- if (us.Length > 1) Password = us[1];
- }
+ base.OnInit();
}
#endregion
#region 登录
- /// <summary>连接后自动登录</summary>
- /// <param name="client">客户端</param>
- /// <param name="force">强制登录</param>
- protected override async Task<Object> OnLoginAsync(ISocketClient client, Boolean force)
+ /// <summary>创建登录请求</summary>
+ /// <returns></returns>
+ public override ILoginRequest BuildLoginRequest()
{
- if (Logined && !force) return null;
+ var request = new LoginModel();
+ FillLoginRequest(request);
var asmx = AssemblyX.Entry;
var title = asmx?.Asm.GetCustomAttribute<AssemblyTitleAttribute>();
@@ -76,61 +80,46 @@ public class AntClient : ApiClient
var des = asmx?.Asm.GetCustomAttribute<DescriptionAttribute>();
var dname = title?.Title ?? dis?.DisplayName ?? des?.Description;
- var arg = new LoginModel
- {
- User = UserName,
- Pass = Password.IsNullOrEmpty() ? null : Password.MD5(),
- DisplayName = dname,
- Machine = Environment.MachineName,
- ProcessId = Process.GetCurrentProcess().Id,
- Version = asmx.Version,
- Compile = asmx.Compile,
- };
-
- var rs = await base.InvokeWithClientAsync<LoginResponse>(client, "Login", arg);
-
- var set = AntSetting.Current;
- if (set.Debug) XTrace.WriteLine("登录{0}成功!{1}", client, rs.ToJson());
-
- // 保存下发密钥
- if (!rs.Secret.IsNullOrEmpty())
- {
- set.Secret = rs.Secret;
- set.Save();
- }
-
- Logined = true;
+ request.DisplayName = dname;
+ request.Machine = Environment.MachineName;
+ request.ProcessId = Process.GetCurrentProcess().Id;
+ //request.Compile = asmx.Compile;
- return Info = rs;
+ return request;
}
#endregion
#region 核心方法
/// <summary>获取指定名称的作业</summary>
/// <returns></returns>
- public IJob[] GetJobs() => Invoke<JobModel[]>(nameof(GetJobs));
+ public IJob[] GetJobs() => InvokeAsync<JobModel[]>(nameof(GetJobs)).Result;
/// <summary>批量添加作业</summary>
/// <param name="jobs"></param>
/// <returns></returns>
- public String[] AddJobs(IJob[] jobs) => Invoke<String[]>(nameof(AddJobs), new { jobs });
+ public String[] AddJobs(IJob[] jobs) => InvokeAsync<String[]>(nameof(AddJobs), new { jobs }).Result;
+
+ /// <summary>设置作业。支持控制作业启停、数据时间、步进等参数</summary>
+ /// <param name="job"></param>
+ /// <returns></returns>
+ public IJob SetJob(IDictionary<String, Object> job) => InvokeAsync<JobModel>(nameof(SetJob), job).Result;
/// <summary>申请作业任务</summary>
/// <param name="job">作业</param>
/// <param name="topic">主题</param>
/// <param name="count">要申请的任务个数</param>
/// <returns></returns>
- public ITask[] Acquire(String job, String topic, Int32 count) => Invoke<TaskModel[]>(nameof(Acquire), new AcquireModel
+ public ITask[] Acquire(String job, String topic, Int32 count) => InvokeAsync<TaskModel[]>(nameof(Acquire), new AcquireModel
{
Job = job,
Topic = topic,
Count = count,
- });
+ }).Result;
/// <summary>生产消息</summary>
/// <param name="model">模型</param>
/// <returns></returns>
- public Int32 Produce(ProduceModel model) => Invoke<Int32>(nameof(Produce), model);
+ public Int32 Produce(ProduceModel model) => InvokeAsync<Int32>(nameof(Produce), model).Result;
/// <summary>报告状态(进度、成功、错误)</summary>
/// <param name="task"></param>
@@ -143,7 +132,7 @@ public class AntClient : ApiClient
{
try
{
- return Invoke<Boolean>(nameof(Report), task);
+ return InvokeAsync<Boolean>(nameof(Report), task).Result;
}
catch (Exception ex)
{
@@ -156,6 +145,6 @@ public class AntClient : ApiClient
/// <summary>获取当前应用的所有在线实例</summary>
/// <returns></returns>
- public IPeer[] GetPeers() => Invoke<PeerModel[]>(nameof(GetPeers));
+ public IPeer[] GetPeers() => InvokeAsync<PeerModel[]>(nameof(GetPeers)).Result;
#endregion
}
\ No newline at end of file
diff --git a/AntJob/Providers/FileJobProvider.cs b/AntJob/Providers/FileJobProvider.cs
index 30f6038..b8f4a53 100644
--- a/AntJob/Providers/FileJobProvider.cs
+++ b/AntJob/Providers/FileJobProvider.cs
@@ -43,7 +43,7 @@ public class FileJobProvider : JobProvider
var df = job?.Job;
if (df != null) model.Copy(df);
- if (model.Start.Year <= 2000) model.Start = DateTime.Now.Date;
+ if (model.DataTime.Year <= 2000) model.DataTime = DateTime.Now.Date;
if (model.Step <= 0) model.Step = 30;
if (model.BatchSize <= 0) model.BatchSize = 10000;
if (model.MaxTask <= 0) model.MaxTask = Environment.ProcessorCount;
@@ -88,6 +88,11 @@ public class FileJobProvider : JobProvider
return list.ToArray();
}
+ /// <summary>设置作业。支持控制作业启停、数据时间、步进等参数</summary>
+ /// <param name="job"></param>
+ /// <returns></returns>
+ public override IJob SetJob(IJob job) => null;
+
/// <summary>申请任务</summary>
/// <param name="job">作业</param>
/// <param name="topic">主题</param>
@@ -108,7 +113,7 @@ public class FileJobProvider : JobProvider
var step = job.Step;
if (step <= 0) step = 30;
- var start = job.Start;
+ var start = job.DataTime;
for (var i = 0; i < count; i++)
{
// 开始时间和结束时间是否越界
@@ -119,7 +124,7 @@ public class FileJobProvider : JobProvider
if (job.End.Year > 2000 && end > job.End) end = job.End;
// 时间片必须严格要求按照步进大小分片,除非有合适的End
- if (job.Mode != JobModes.Alarm)
+ if (job.Mode != JobModes.Time)
{
if (end > now) break;
}
@@ -128,9 +133,9 @@ public class FileJobProvider : JobProvider
if (start >= end) break;
// 切分新任务
- var set = new TaskModel
+ var task = new TaskModel
{
- Start = start,
+ DataTime = start,
End = end,
//Step = job.Step,
//Offset = job.Offset,
@@ -138,10 +143,10 @@ public class FileJobProvider : JobProvider
};
// 更新任务
- job.Start = end;
+ job.DataTime = end;
start = end;
- list.Add(set);
+ list.Add(task);
}
if (list.Count > 0)
@@ -163,13 +168,16 @@ public class FileJobProvider : JobProvider
if (ctx.Total > 0)
{
var set = ctx.Task;
+ var time = set.DataTime;
+ var end = set.End;
var n = 0;
- if (set.End > set.Start) n = (Int32)(set.End - set.Start).TotalSeconds;
- var msg = $"{ctx.Handler.Name} 处理{ctx.Total:n0} 行,区间({set.Start} + {n}, {set.End:HH:mm:ss})";
- if (ctx.Handler.Mode == JobModes.Alarm)
+ if (end > time) n = (Int32)(end - time).TotalSeconds;
+ var msg = $"{ctx.Handler.Name} 处理{ctx.Total:n0} 行,区间({time} + {n}, {end:HH:mm:ss})";
+ if (ctx.Handler.Mode == JobModes.Time)
msg += $",耗时{ctx.Cost:n0}ms";
else
msg += $",速度{ctx.Speed:n0}tps,耗时{ctx.Cost:n0}ms";
+
XTrace.WriteLine(msg);
}
}
diff --git a/AntJob/Providers/HttpJobProvider.cs b/AntJob/Providers/HttpJobProvider.cs
index d960fa5..74058a3 100644
--- a/AntJob/Providers/HttpJobProvider.cs
+++ b/AntJob/Providers/HttpJobProvider.cs
@@ -186,6 +186,8 @@ public class HttpJobProvider : JobProvider
task.Total = ctx.Total;
task.Success = ctx.Success;
+ if (ctx.NextTime.Year > 2000) task.NextTime = ctx.NextTime.ToUniversalTime();
+
Report(ctx.Handler.Job, task);
}
@@ -214,11 +216,11 @@ public class HttpJobProvider : JobProvider
task.Message = msg;
}
}
- else
+ else if (task.Status <= JobStatus.处理中)
{
task.Status = JobStatus.完成;
- task.Cost = (Int32)Math.Round(ctx.Cost / 1000);
}
+ task.Cost = (Int32)Math.Round(ctx.Cost / 1000);
if (task.Message.IsNullOrEmpty()) task.Message = ctx.Remark;
task.Key = ctx.Key;
@@ -234,7 +236,7 @@ public class HttpJobProvider : JobProvider
}
catch (Exception ex)
{
- XTrace.WriteLine("[{0}]的[{1}]状态报告失败!{2}", job, task.Status, ex.GetTrue().Message);
+ WriteLog("[{0}]的[{1}]状态报告失败!{2}", job, task.Status, ex.GetTrue().Message);
}
}
#endregion
@@ -246,18 +248,18 @@ public class HttpJobProvider : JobProvider
var ps = Client?.Get<PeerModel[]>("/AntJob/GetPeers");
if (ps == null || ps.Length == 0) return;
- var old = (Peers ?? new IPeer[0]).ToList();
+ var old = (Peers ?? []).ToList();
foreach (var item in ps)
{
var pr = old.FirstOrDefault(e => e.Instance == item.Instance);
if (pr == null)
- XTrace.WriteLine("[{0}]上线!{1}", item.Instance, item.Machine);
+ WriteLog("[{0}]上线!{1}", item.Instance, item.Machine);
else
old.Remove(pr);
}
foreach (var item in old)
{
- XTrace.WriteLine("[{0}]下线!{1}", item.Instance, item.Machine);
+ WriteLog("[{0}]下线!{1}", item.Instance, item.Machine);
}
Peers = ps;
diff --git a/AntJob/Providers/IJobProvider.cs b/AntJob/Providers/IJobProvider.cs
index 156ba9d..9b2a006 100644
--- a/AntJob/Providers/IJobProvider.cs
+++ b/AntJob/Providers/IJobProvider.cs
@@ -1,5 +1,6 @@
using AntJob.Data;
using NewLife;
+using NewLife.Log;
namespace AntJob.Providers;
@@ -19,6 +20,11 @@ public interface IJobProvider
/// <returns></returns>
IJob[] GetJobs();
+ /// <summary>设置作业。支持控制作业启停、数据时间、步进等参数</summary>
+ /// <param name="job"></param>
+ /// <returns></returns>
+ IJob SetJob(IJob job);
+
/// <summary>申请任务</summary>
/// <param name="job">作业</param>
/// <param name="topic">主题</param>
@@ -44,7 +50,7 @@ public interface IJobProvider
}
/// <summary>任务提供者基类</summary>
-public abstract class JobProvider : DisposeBase, IJobProvider
+public abstract class JobProvider : DisposeBase, IJobProvider, ITracerFeature, ILogFeature
{
/// <summary>调度器</summary>
public Scheduler Schedule { get; set; }
@@ -59,6 +65,11 @@ public abstract class JobProvider : DisposeBase, IJobProvider
/// <returns></returns>
public abstract IJob[] GetJobs();
+ /// <summary>设置作业。支持控制作业启停、数据时间、步进等参数</summary>
+ /// <param name="job"></param>
+ /// <returns></returns>
+ public abstract IJob SetJob(IJob job);
+
/// <summary>申请任务</summary>
/// <param name="job">作业</param>
/// <param name="topic">主题</param>
@@ -81,4 +92,17 @@ public abstract class JobProvider : DisposeBase, IJobProvider
/// <summary>完成任务,每个任务只调用一次</summary>
/// <param name="ctx">上下文</param>
public virtual void Finish(JobContext ctx) { }
+
+ #region 日志
+ /// <summary>性能跟踪器</summary>
+ public ITracer Tracer { get; set; }
+
+ /// <summary>日志</summary>
+ public ILog Log { get; set; } = Logger.Null;
+
+ /// <summary>写日志</summary>
+ /// <param name="format"></param>
+ /// <param name="args"></param>
+ public void WriteLog(String format, params Object[] args) => Log?.Info(format, args);
+ #endregion
}
\ No newline at end of file
diff --git a/AntJob/Providers/NetworkJobProvider.cs b/AntJob/Providers/NetworkJobProvider.cs
index 537aec0..da2d5a0 100644
--- a/AntJob/Providers/NetworkJobProvider.cs
+++ b/AntJob/Providers/NetworkJobProvider.cs
@@ -2,35 +2,19 @@
using AntJob.Handlers;
using AntJob.Models;
using NewLife;
-using NewLife.Log;
using NewLife.Threading;
namespace AntJob.Providers;
/// <summary>网络任务提供者</summary>
-public class NetworkJobProvider : JobProvider
+public class NetworkJobProvider(AntSetting setting) : JobProvider
{
#region 属性
- /// <summary>调试,打开编码日志</summary>
- public Boolean Debug { get; set; }
-
- /// <summary>调度中心地址</summary>
- public String Server { get; set; }
-
- /// <summary>应用编号</summary>
- public String AppId { get; set; }
-
- /// <summary>应用密钥</summary>
- public String Secret { get; set; }
-
/// <summary>客户端</summary>
public AntClient Ant { get; set; }
/// <summary>邻居伙伴。用于应用判断自身有多少个实例在运行</summary>
public IPeer[] Peers { get; private set; }
-
- /// <summary>性能跟踪器</summary>
- public ITracer Tracer { get; set; }
#endregion
#region 构造
@@ -40,6 +24,8 @@ public class NetworkJobProvider : JobProvider
{
base.Dispose(disposing);
+ Stop();
+
_timer.TryDispose();
_timer = null;
}
@@ -49,17 +35,15 @@ public class NetworkJobProvider : JobProvider
/// <summary>开始</summary>
public override void Start()
{
- var svr = Server;
+ WriteLog("正在连接调度中心:{0}", setting.Server);
// 使用配置中心账号
- var ant = new AntClient(svr)
+ var ant = new AntClient(setting)
{
- UserName = AppId,
- Password = Secret,
Tracer = Tracer,
+ Log = Log,
};
- if (Debug) ant.EncoderLog = XTrace.Log;
- ant.Open();
+ ant.Login().Wait();
// 断开前一个连接
Ant.TryDispose();
@@ -72,6 +56,16 @@ public class NetworkJobProvider : JobProvider
var list = new List<IJob>();
foreach (var handler in bs)
{
+ // 初始化处理器
+ try
+ {
+ handler.Init();
+ }
+ catch (Exception ex)
+ {
+ Log?.Error(ex.Message);
+ }
+
var job = handler.Job ?? new JobModel();
job.Name = handler.Name;
@@ -89,9 +83,34 @@ public class NetworkJobProvider : JobProvider
if (handler is MessageHandler mhandler) job2.Topic = mhandler.Topic;
}
+ // 改为UTC通信
+ if (job.DataTime.Year > 1000)
+ job.DataTime = job.DataTime.ToUniversalTime();
+ if (job.End.Year > 1000)
+ job.End = job.End.ToUniversalTime();
+
list.Add(job);
}
- if (list.Count > 0) Ant.AddJobs(list.ToArray());
+ if (list.Count > 0)
+ {
+ WriteLog("注册作业[{0}]:{1}", list.Count, list.Join(",", e => e.Name));
+
+ var rs = Ant.AddJobs(list.ToArray());
+
+ WriteLog("注册成功[{0}]:{1}", rs?.Length, rs.Join());
+ }
+
+ // 通信完成,改回来本地时间
+ foreach (var handler in bs)
+ {
+ var job = handler.Job;
+ if (job != null)
+ {
+ job.DataTime = job.DataTime.ToLocalTime();
+ if (job.End.Year > 1000)
+ job.End = job.End.ToLocalTime();
+ }
+ }
// 定时更新邻居
_timer = new TimerX(DoCheckPeer, null, 1_000, 30_000) { Async = true };
@@ -100,6 +119,8 @@ public class NetworkJobProvider : JobProvider
/// <summary>停止</summary>
public override void Stop()
{
+ Ant?.Logout(nameof(Stop)).Wait(1_000);
+
// 断开前一个连接
Ant.TryDispose();
Ant = null;
@@ -108,6 +129,7 @@ public class NetworkJobProvider : JobProvider
#region 作业消息控制
private IJob[] _jobs;
+ private IJob[] _baks;
private DateTime _NextGetJobs;
/// <summary>获取所有作业名称</summary>
/// <returns></returns>
@@ -120,17 +142,68 @@ public class NetworkJobProvider : JobProvider
_NextGetJobs = now.AddSeconds(5);
_jobs = Ant.GetJobs();
+
+ if (_jobs != null)
+ {
+ foreach (var job in _jobs)
+ {
+ // 通信约定UTC,收到后需转为本地时间
+ job.DataTime = job.DataTime.ToLocalTime();
+ if (job.End.Year > 1000)
+ job.End = job.End.ToLocalTime();
+ }
+
+ // 备份一份,用于比较
+ _baks = _jobs.Select(e => ((ICloneable)e).Clone() as IJob).ToArray();
+ }
}
return _jobs;
}
+ /// <summary>设置作业。支持控制作业启停、数据时间、步进等参数</summary>
+ /// <param name="job"></param>
+ /// <returns></returns>
+ public override IJob SetJob(IJob job)
+ {
+ var dic = job.ToDictionary();
+ var old = _baks?.FirstOrDefault(e => e.Name == job.Name);
+ old ??= new JobModel();
+
+ var dic2 = old.ToDictionary();
+ foreach (var item in dic2)
+ {
+ if (item.Key == nameof(job.Name)) continue;
+
+ // 未修改的不要传递过去
+ if (dic.TryGetValue(item.Key, out var value) && Equals(value, item.Value))
+ dic.Remove(item.Key);
+ }
+
+ return Ant.SetJob(dic);
+ }
+
/// <summary>申请任务</summary>
/// <param name="job">作业</param>
/// <param name="topic">主题</param>
/// <param name="count">要申请的任务个数</param>
/// <returns></returns>
- public override ITask[] Acquire(IJob job, String topic, Int32 count) => Ant.Acquire(job.Name, topic, count);
+ public override ITask[] Acquire(IJob job, String topic, Int32 count)
+ {
+ var rs = Ant.Acquire(job.Name, topic, count);
+ if (rs != null)
+ {
+ foreach (var task in rs)
+ {
+ // 通信约定UTC,收到后需转为本地时间
+ task.DataTime = task.DataTime.ToLocalTime();
+ if (task.End.Year > 1000)
+ task.End = task.End.ToLocalTime();
+ }
+ }
+
+ return rs;
+ }
/// <summary>生产消息</summary>
/// <param name="job">作业</param>
@@ -159,9 +232,6 @@ public class NetworkJobProvider : JobProvider
#endregion
#region 报告状态
- //private static readonly String _MachineName = Environment.MachineName;
- //private static readonly Int32 _ProcessID = Process.GetCurrentProcess().Id;
-
/// <summary>报告进度,每个任务多次调用</summary>
/// <param name="ctx">上下文</param>
public override void Report(JobContext ctx)
@@ -178,6 +248,8 @@ public class NetworkJobProvider : JobProvider
task.Total = ctx.Total;
task.Success = ctx.Success;
+ if (ctx.NextTime.Year > 2000) task.NextTime = ctx.NextTime.ToUniversalTime();
+
Report(ctx.Handler.Job, task);
}
@@ -192,6 +264,8 @@ public class NetworkJobProvider : JobProvider
task.Success = ctx.Success;
task.Times++;
+ if (ctx.NextTime.Year > 2000) task.NextTime = ctx.NextTime.ToUniversalTime();
+
// 区分正常完成还是错误终止
if (ctx.Error != null)
{
@@ -202,15 +276,21 @@ public class NetworkJobProvider : JobProvider
if (ex != null)
{
var msg = ctx.Error.GetMessage();
- if (msg.Contains("Exception:")) msg = msg.Substring("Exception:").Trim();
+ var p = msg.IndexOf("Exception:");
+ if (p >= 0) msg = msg.Substring(p + "Exception:".Length).Trim();
task.Message = msg;
}
}
+ else if (ctx.Status == JobStatus.延迟)
+ {
+ task.Status = JobStatus.延迟;
+ }
else
{
task.Status = JobStatus.完成;
- task.Cost = (Int32)Math.Round(ctx.Cost / 1000);
}
+
+ task.Cost = (Int32)Math.Round(ctx.Cost);
if (task.Message.IsNullOrEmpty()) task.Message = ctx.Remark;
task.Key = ctx.Key;
@@ -226,30 +306,31 @@ public class NetworkJobProvider : JobProvider
}
catch (Exception ex)
{
- XTrace.WriteLine("[{0}]的[{1}]状态报告失败!{2}", job, task.Status, ex.GetTrue().Message);
+ WriteLog("[{0}]的[{1}]状态报告失败!{2}", job, task.Status, ex.GetTrue().Message);
}
}
#endregion
#region 邻居
private TimerX _timer;
+
private void DoCheckPeer(Object state)
{
var ps = Ant?.GetPeers();
if (ps == null || ps.Length == 0) return;
- var old = (Peers ?? new IPeer[0]).ToList();
+ var old = (Peers ?? []).ToList();
foreach (var item in ps)
{
var pr = old.FirstOrDefault(e => e.Instance == item.Instance);
if (pr == null)
- XTrace.WriteLine("[{0}]上线!{1}", item.Instance, item.Machine);
+ WriteLog("[{0}]上线!{1}", item.Instance, item.Machine);
else
old.Remove(pr);
}
foreach (var item in old)
{
- XTrace.WriteLine("[{0}]下线!{1}", item.Instance, item.Machine);
+ WriteLog("[{0}]下线!{1}", item.Instance, item.Machine);
}
Peers = ps;
diff --git a/AntJob/Scheduler.cs b/AntJob/Scheduler.cs
index 06a69aa..cd42b7b 100644
--- a/AntJob/Scheduler.cs
+++ b/AntJob/Scheduler.cs
@@ -2,13 +2,12 @@
using AntJob.Handlers;
using AntJob.Providers;
using NewLife;
+using NewLife.Caching;
using NewLife.Log;
using NewLife.Model;
using NewLife.Reflection;
-using NewLife.Remoting;
using NewLife.Threading;
using Stardust.Registry;
-using static System.Net.WebRequestMethods;
namespace AntJob;
@@ -17,7 +16,7 @@ public class Scheduler : DisposeBase
{
#region 属性
/// <summary>处理器集合</summary>
- public List<Handler> Handlers { get; } = new List<Handler>();
+ public List<Handler> Handlers { get; } = [];
/// <summary>作业提供者</summary>
public IJobProvider Provider { get; set; }
@@ -25,8 +24,7 @@ public class Scheduler : DisposeBase
/// <summary>服务提供者</summary>
public IServiceProvider ServiceProvider { get; set; }
- /// <summary>性能跟踪器</summary>
- public ITracer Tracer { get; set; }
+ private ICache _cache = MemoryCache.Default;
#endregion
#region 构造
@@ -59,52 +57,47 @@ public class Scheduler : DisposeBase
#region 核心方法
/// <summary>加入调度中心,从注册中心获取地址,自动识别RPC/Http</summary>
- /// <param name="server"></param>
- /// <param name="appId"></param>
- /// <param name="secret"></param>
- /// <param name="debug"></param>
+ /// <param name="set"></param>
/// <returns></returns>
- public IJobProvider Join(String server, String appId, String secret, Boolean debug = false)
+ public IJobProvider Join(AntSetting set)
{
+ var server = set.Server;
+
var registry = ServiceProvider?.GetService<IRegistry>();
if (registry != null)
{
+ var rs = registry.ResolveAsync("AntServer").Result;
var svrs = registry.ResolveAddressAsync("AntServer").Result;
if (svrs != null && svrs.Length > 0) server = svrs.Join();
}
if (server.IsNullOrEmpty()) return null;
+ set.Server = server;
// 根据地址决定用Http还是RPC
- var servers = server.Split(",");
- if (servers.Any(e => e.StartsWithIgnoreCase("http://", "https://")))
- {
- var http = new HttpJobProvider
- {
- Debug = debug,
- Server = server,
- AppId = appId,
- Secret = secret,
- };
-
- // 如果有注册中心,则使用注册中心的服务发现
- if (registry != null)
- {
- //http.Client = registry.CreateForService("AntServer") as ApiHttpClient;
- //http.Client.RoundRobin = false;
- }
+ //var servers = server.Split(",");
+ //if (servers.Any(e => e.StartsWithIgnoreCase("http://", "https://")))
+ //{
+ // var http = new HttpJobProvider
+ // {
+ // Debug = debug,
+ // Server = server,
+ // AppId = appId,
+ // Secret = secret,
+ // };
+
+ // // 如果有注册中心,则使用注册中心的服务发现
+ // if (registry != null)
+ // {
+ // //http.Client = registry.CreateForService("AntServer") as ApiHttpClient;
+ // //http.Client.RoundRobin = false;
+ // }
- Provider = http;
- }
- else
+ // Provider = http;
+ //}
+ //else
{
- var rpc = new NetworkJobProvider
- {
- Debug = debug,
- Server = server,
- AppId = appId,
- Secret = secret,
- };
+ var rpc = new NetworkJobProvider(set);
Provider = rpc;
}
@@ -115,11 +108,44 @@ public class Scheduler : DisposeBase
/// <summary>开始</summary>
public void Start()
{
+ OnStart();
+ }
+
+ /// <summary>异步开始。使用定时器尝试连接服务端</summary>
+ public void StartAsync()
+ {
+ _timerStart = new TimerX(CheckStart, null, 100, 15000, "Job") { Async = true };
+ }
+
+ private Boolean _inited;
+ private TimerX _timerStart;
+ private void CheckStart(Object state)
+ {
+ if (!_inited)
+ {
+ try
+ {
+ OnStart();
+
+ _inited = true;
+ }
+ catch (Exception ex)
+ {
+ Log?.Error(ex.Message);
+ }
+ }
+
+ if (_inited) _timerStart.TryDispose();
+ }
+
+ private void OnStart()
+ {
// 检查本地添加的处理器
var hs = Handlers;
if (hs.Count == 0) throw new ArgumentNullException(nameof(Handlers), "没有可用处理器");
// 埋点
+ Tracer ??= ServiceProvider?.GetService<ITracer>();
using var span = Tracer?.NewSpan("job:SchedulerStart");
// 启动作业提供者
@@ -127,32 +153,17 @@ public class Scheduler : DisposeBase
prv ??= Provider = new FileJobProvider();
prv.Schedule ??= this;
- //if (prv is NetworkJobProvider network)
- //{
- // network.Tracer ??= Tracer;
-
- // // 从注册中心获取服务端地址,优先于本地配置文件
- // if (network.Server.IsNullOrEmpty() || network.Server.EqualIgnoreCase(AntSetting.Current.Server))
- // {
- // // 从注册中心获取包
- // var registry = ServiceProvider?.GetService<IRegistry>();
- // if (registry != null)
- // {
- // var svrs = registry.ResolveAddressAsync("AntServer").Result;
- // if (svrs != null && svrs.Length > 0) network.Server = svrs.Join();
- // }
- // }
- //}
+ if (prv is ITracerFeature tf) tf.Tracer = Tracer;
+ if (prv is ILogFeature lf) lf.Log = Log;
prv.Start();
// 获取本应用在调度中心管理的所有作业
- var jobs = prv.GetJobs();
- if (jobs == null || jobs.Length == 0) throw new Exception("调度中心没有可用作业");
+ var jobs = prv.GetJobs() ?? [];
+ //if (jobs == null || jobs.Length == 0) throw new Exception("调度中心没有可用作业");
// 输出日志
- var msg = $"启动任务调度引擎[{prv}],作业[{hs.Count}]项,定时{Period}秒";
- XTrace.WriteLine(msg);
+ WriteLog($"启动任务调度引擎[{prv}],作业[{hs.Count}]项,定时{Period}秒");
// 设置日志
foreach (var handler in hs)
@@ -162,11 +173,22 @@ public class Scheduler : DisposeBase
// 查找作业参数,分配给处理器
var job = jobs.FirstOrDefault(e => e.Name == handler.Name);
+ if (job == null || !job.Enable) continue;
+
if (job != null && job.Mode == 0) job.Mode = handler.Mode;
handler.Job = job;
- handler.Log = XTrace.Log;
- handler.Start();
+ handler.Tracer = Tracer;
+ handler.Log = Log;
+
+ try
+ {
+ handler.Start();
+ }
+ catch (Exception ex)
+ {
+ Log?.Error("作业[{0}]启动失败!{1}", handler.GetType().FullName, ex.Message);
+ }
}
// 定时执行
@@ -185,7 +207,14 @@ public class Scheduler : DisposeBase
foreach (var handler in Handlers)
{
- handler.Stop("SchedulerStop");
+ try
+ {
+ handler.Stop("SchedulerStop");
+ }
+ catch (Exception ex)
+ {
+ Log?.Error("作业[{0}]停止失败!{1}", handler.GetType().FullName, ex.Message);
+ }
}
}
@@ -224,7 +253,17 @@ public class Scheduler : DisposeBase
// 更新作业参数,并启动处理器
handler.Job = job;
if (job.Mode == 0) job.Mode = handler.Mode;
- if (!handler.Active) handler.Start();
+ if (!handler.Active)
+ {
+ try
+ {
+ handler.Start();
+ }
+ catch (Exception ex)
+ {
+ Log?.Error("作业[{0}]启动失败!{1}", handler.GetType().FullName, ex.Message);
+ }
+ }
// 如果正在处理任务数没达到最大并行度,则继续安排任务
var max = job.MaxTask;
@@ -267,9 +306,12 @@ public class Scheduler : DisposeBase
var handler = handlers.FirstOrDefault(e => e.Name == job.Name);
if (handler == null && job.Enable && !job.ClassName.IsNullOrEmpty())
{
+ // 遇到废弃作业时,避免反复输出日志
+ if (!_cache.Add($"job:NewHandler:{job.Name}", 1, 3600)) return;
+
using var span = Tracer?.NewSpan($"job:NewHandler", job);
- XTrace.WriteLine("发现未知作业[{0}]@[{1}]", job.Name, job.ClassName);
+ WriteLog("发现未知作业[{0}]@[{1}]", job.Name, job.ClassName);
try
{
// 实例化一个处理器
@@ -279,15 +321,16 @@ public class Scheduler : DisposeBase
handler = type.CreateInstance() as Handler;
if (handler != null)
{
- XTrace.WriteLine("添加新作业[{0}]@[{1}]", job.Name, job.ClassName);
+ WriteLog("添加新作业[{0}]@[{1}]", job.Name, job.ClassName);
handler.Name = job.Name;
handler.Schedule = this;
handler.Provider = provider;
- if (handler is MessageHandler messageHandler && !job.Topic.IsNullOrEmpty()) messageHandler.Topic = job.Topic;
+ if (handler is MessageHandler messageHandler && !job.Topic.IsNullOrEmpty())
+ messageHandler.Topic = job.Topic;
- handler.Log = XTrace.Log;
+ handler.Log = Log;
handler.Tracer = Tracer;
handler.Start();
@@ -298,7 +341,8 @@ public class Scheduler : DisposeBase
catch (Exception ex)
{
span?.SetError(ex, null);
- XTrace.WriteException(ex);
+ //XTrace.WriteException(ex);
+ Log?.Error("作业[{0}]启动失败!{1}", handler?.GetType().FullName, ex.Message);
}
}
}
@@ -324,4 +368,17 @@ public class Scheduler : DisposeBase
if (rs) TimerX.Current.SetNext(-1);
}
#endregion
+
+ #region 日志
+ /// <summary>性能跟踪器</summary>
+ public ITracer Tracer { get; set; }
+
+ /// <summary>日志</summary>
+ public ILog Log { get; set; } = Logger.Null;
+
+ /// <summary>写日志</summary>
+ /// <param name="format"></param>
+ /// <param name="args"></param>
+ public void WriteLog(String format, params Object[] args) => Log?.Info(format, args);
+ #endregion
}
\ No newline at end of file
diff --git a/AntTest/AntTest.csproj b/AntTest/AntTest.csproj
index 42c381f..1535955 100644
--- a/AntTest/AntTest.csproj
+++ b/AntTest/AntTest.csproj
@@ -9,10 +9,10 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
- <PackageReference Include="NewLife.UnitTest" Version="1.0.2023.1204" />
- <PackageReference Include="xunit" Version="2.7.0" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
+ <PackageReference Include="NewLife.UnitTest" Version="1.0.2024.1006" />
+ <PackageReference Include="xunit" Version="2.9.2" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
diff --git a/AntTest/SqlHandlerTests.cs b/AntTest/SqlHandlerTests.cs
index 05f3dfa..0161180 100644
--- a/AntTest/SqlHandlerTests.cs
+++ b/AntTest/SqlHandlerTests.cs
@@ -1,9 +1,7 @@
-using System;
-using System.Linq;
+using System.Linq;
using AntJob;
using AntJob.Data;
using AntJob.Extensions;
-using NewLife.Reflection;
using XCode.DataAccessLayer;
using XCode.Membership;
using Xunit;
@@ -44,11 +42,13 @@ public class SqlHandlerTests
Task = task,
};
- var method = handler.GetType().GetMethodEx("OnProcess", typeof(JobContext));
- method.Invoke(handler, new Object[] { ctx });
+ //var method = handler.GetType().GetMethodEx("OnProcess", typeof(JobContext));
+ //method.Invoke(handler, new Object[] { ctx });
+ var rs = handler.Execute(ctx);
Assert.Equal(4, ctx.Total);
//Assert.Equal(4, ctx.Success);
- Assert.True(ctx.Success > 0);
+ Assert.Equal(8, rs);
+ //Assert.True(ctx.Success > 0);
}
}
diff --git a/AntTest/SqlSectionTests.cs b/AntTest/SqlSectionTests.cs
index 6104fa0..9fbb505 100644
--- a/AntTest/SqlSectionTests.cs
+++ b/AntTest/SqlSectionTests.cs
@@ -9,7 +9,7 @@ namespace AntTest
public void ParseQuery()
{
var tt = @"/*use his*/
-select * from t1 where time between '{Start}' and '{End}'
+select * from t1 where time between '{dt}' and '{End}'
";
var section = new SqlSection();
@@ -17,7 +17,7 @@ select * from t1 where time between '{Start}' and '{End}'
Assert.Equal("his", section.ConnName);
Assert.Equal(SqlActions.Query, section.Action);
- Assert.Equal("select * from t1 where time between '{Start}' and '{End}'", section.Sql);
+ Assert.Equal("select * from t1 where time between '{dt}' and '{End}'", section.Sql);
}
[Fact]
@@ -39,7 +39,7 @@ insert into t1 (c1, c2) values(v1, v2);
public void ParseDelete()
{
var tt = @"/*use his*/
-delete from t2 where time between '{Start}' and '{End}';
+delete from t2 where time between '{dt}' and '{End}';
";
var section = new SqlSection();
@@ -47,7 +47,7 @@ delete from t2 where time between '{Start}' and '{End}';
Assert.Equal("his", section.ConnName);
Assert.Equal(SqlActions.Execute, section.Action);
- Assert.Equal("delete from t2 where time between '{Start}' and '{End}'", section.Sql);
+ Assert.Equal("delete from t2 where time between '{dt}' and '{End}'", section.Sql);
}
[Fact]
@@ -85,10 +85,10 @@ insert t2;
public void ParseAllSqls()
{
var tt = @"/*use his*/
-select * from t1 where time between '{Start}' and '{End}'
+select * from t1 where time between '{dt}' and '{End}'
/*use his_bak*/
-delete from t2 where time between '{Start}' and '{End}';
+delete from t2 where time between '{dt}' and '{End}';
/*use his_bak*/
insert t2;
@@ -100,11 +100,11 @@ insert t2;
Assert.Equal("his", cs[0].ConnName);
Assert.Equal(SqlActions.Query, cs[0].Action);
- Assert.Equal("select * from t1 where time between '{Start}' and '{End}'", cs[0].Sql);
+ Assert.Equal("select * from t1 where time between '{dt}' and '{End}'", cs[0].Sql);
Assert.Equal("his_bak", cs[1].ConnName);
Assert.Equal(SqlActions.Execute, cs[1].Action);
- Assert.Equal("delete from t2 where time between '{Start}' and '{End}'", cs[1].Sql);
+ Assert.Equal("delete from t2 where time between '{dt}' and '{End}'", cs[1].Sql);
Assert.Equal("his_bak", cs[2].ConnName);
Assert.Equal(SqlActions.Insert, cs[2].Action);
diff --git a/AntTest/TemplateHelperTests.cs b/AntTest/TemplateHelperTests.cs
index 3c40344..6d327c1 100644
--- a/AntTest/TemplateHelperTests.cs
+++ b/AntTest/TemplateHelperTests.cs
@@ -2,95 +2,94 @@
using AntJob.Data;
using Xunit;
-namespace AntTest
+namespace AntTest;
+
+public class TemplateHelperTests
{
- public class TemplateHelperTests
+ [Fact]
+ public void BuildTest()
{
- [Fact]
- public void BuildTest()
- {
- var tt = @"/*use His*/
-insert into t1(xxx) select * from t2 where time between {Start} and {End}";
- var start = DateTime.Now;
- var end = start.AddSeconds(30);
+ var tt = @"/*use His*/
+insert into t1(xxx) select * from t2 where time between {dt} and {End}";
+ var start = DateTime.Now;
+ var end = start.AddSeconds(30);
- var str = TemplateHelper.Build(tt, start, end);
- Assert.NotNull(str);
- Assert.NotEmpty(str);
- Assert.DoesNotContain("{Start}", str);
- Assert.DoesNotContain("{End}", str);
+ var str = TemplateHelper.Build(tt, start, end);
+ Assert.NotNull(str);
+ Assert.NotEmpty(str);
+ Assert.DoesNotContain("{dt}", str);
+ Assert.DoesNotContain("{End}", str);
- var rs = tt.Replace("{Start}", start.ToFullString()).Replace("{End}", end.ToFullString());
- Assert.Equal(rs, str);
- }
+ var rs = tt.Replace("{dt}", start.ToFullString()).Replace("{End}", end.ToFullString());
+ Assert.Equal(rs, str);
+ }
- [Fact]
- public void BuildTest2()
- {
- var tt = @"/*use His*/
-insert into t1(xxx) select * from t2 where time between {Start:yyMMdd} and {End:HH:mm:ss}";
- var start = DateTime.Now;
- var end = start.AddSeconds(30);
+ [Fact]
+ public void BuildTest2()
+ {
+ var tt = @"/*use His*/
+insert into t1(xxx) select * from t2 where time between {dt:yyMMdd} and {End:HH:mm:ss}";
+ var start = DateTime.Now;
+ var end = start.AddSeconds(30);
- var str = TemplateHelper.Build(tt, start, end);
- Assert.NotNull(str);
- Assert.NotEmpty(str);
- Assert.DoesNotContain("{Start:yyMMdd}", str);
- Assert.DoesNotContain("{End:HH:mm:ss}", str);
+ var str = TemplateHelper.Build(tt, start, end);
+ Assert.NotNull(str);
+ Assert.NotEmpty(str);
+ Assert.DoesNotContain("{dt:yyMMdd}", str);
+ Assert.DoesNotContain("{End:HH:mm:ss}", str);
- var rs = tt.Replace("{Start:yyMMdd}", start.ToString("yyMMdd")).Replace("{End:HH:mm:ss}", end.ToString("HH:mm:ss"));
- Assert.Equal(rs, str);
- }
+ var rs = tt.Replace("{dt:yyMMdd}", start.ToString("yyMMdd")).Replace("{End:HH:mm:ss}", end.ToString("HH:mm:ss"));
+ Assert.Equal(rs, str);
+ }
- [Fact]
- public void BuildTest3()
- {
- var tt = @"/*use His*/
-insert into t1(xxx) select * from t2 where time between {Start:yyMMdd} and {End:HH:mm:ss} time2 between {Start:yyMMdd} and {End:yyMMdd}";
- var start = DateTime.Now;
- var end = start.AddSeconds(30);
+ [Fact]
+ public void BuildTest3()
+ {
+ var tt = @"/*use His*/
+insert into t1(xxx) select * from t2 where time between {dt:yyMMdd} and {End:HH:mm:ss} time2 between {dt:yyMMdd} and {End:yyMMdd}";
+ var start = DateTime.Now;
+ var end = start.AddSeconds(30);
- var str = TemplateHelper.Build(tt, start, end);
- Assert.NotNull(str);
- Assert.NotEmpty(str);
- Assert.DoesNotContain("{Start:yyMMdd}", str);
- Assert.DoesNotContain("{End:HH:mm:ss}", str);
- Assert.DoesNotContain("{End:yyMMdd}", str);
+ var str = TemplateHelper.Build(tt, start, end);
+ Assert.NotNull(str);
+ Assert.NotEmpty(str);
+ Assert.DoesNotContain("{dt:yyMMdd}", str);
+ Assert.DoesNotContain("{End:HH:mm:ss}", str);
+ Assert.DoesNotContain("{End:yyMMdd}", str);
- var rs = tt
- .Replace("{Start:yyMMdd}", start.ToString("yyMMdd"))
- .Replace("{End:HH:mm:ss}", end.ToString("HH:mm:ss"))
- .Replace("{End:yyMMdd}", end.ToString("yyMMdd"))
- ;
- Assert.Equal(rs, str);
- }
+ var rs = tt
+ .Replace("{dt:yyMMdd}", start.ToString("yyMMdd"))
+ .Replace("{End:HH:mm:ss}", end.ToString("HH:mm:ss"))
+ .Replace("{End:yyMMdd}", end.ToString("yyMMdd"))
+ ;
+ Assert.Equal(rs, str);
+ }
- [Fact]
- public void BuildTest4()
- {
- var tt = @"/*use his*/
-select * from t1 where time between '{Start}' and '{End}'
+ [Fact]
+ public void BuildTest4()
+ {
+ var tt = @"/*use his*/
+select * from t1 where time between '{dt}' and '{End}'
/*use hist_bak*/
-delete from t2 where time between '{Start}' and '{End}';
+delete from t2 where time between '{dt}' and '{End}';
/*use hist_bak*/
/*batchinsert t2*/
";
- var start = DateTime.Now;
- var end = start.AddSeconds(30);
+ var start = DateTime.Now;
+ var end = start.AddSeconds(30);
- var str = TemplateHelper.Build(tt, start, end);
- Assert.NotNull(str);
- Assert.NotEmpty(str);
- Assert.DoesNotContain("{Start}", str);
- Assert.DoesNotContain("{End}", str);
+ var str = TemplateHelper.Build(tt, start, end);
+ Assert.NotNull(str);
+ Assert.NotEmpty(str);
+ Assert.DoesNotContain("{dt}", str);
+ Assert.DoesNotContain("{End}", str);
- var rs = tt
- .Replace("{Start}", start.ToFullString())
- .Replace("{End}", end.ToFullString())
- ;
- Assert.Equal(rs, str);
- }
+ var rs = tt
+ .Replace("{dt}", start.ToFullString())
+ .Replace("{End}", end.ToFullString())
+ ;
+ Assert.Equal(rs, str);
}
}
\ No newline at end of file
diff --git a/AntTest/TimeExpressionTests.cs b/AntTest/TimeExpressionTests.cs
new file mode 100644
index 0000000..c233a28
--- /dev/null
+++ b/AntTest/TimeExpressionTests.cs
@@ -0,0 +1,110 @@
+using System;
+using AntJob.Data;
+using Xunit;
+
+namespace AntTest;
+
+public class TimeExpressionTests
+{
+ [Theory]
+ [InlineData("+1y", "y", 1)]
+ [InlineData("-1y", "y", -1)]
+ [InlineData("+5M", "M", 5)]
+ [InlineData("-5M", "M", -5)]
+ [InlineData("+5d", "d", 5)]
+ [InlineData("-5d", "d", -5)]
+ [InlineData("-0d", "d", 0)]
+ [InlineData("+5H", "H", 5)]
+ [InlineData("-5H", "H", -5)]
+ [InlineData("+5m", "m", 5)]
+ [InlineData("-5m", "m", -5)]
+ [InlineData("+5w", "w", 5)]
+ [InlineData("-5w", "w", -5)]
+ public void ParseItem(String str, String level, Int32 value)
+ {
+ var item = new TimeExpressionItem();
+ var rs = item.Parse(str);
+ Assert.True(rs);
+ Assert.Equal(level, item.Level);
+ Assert.Equal(value, item.Value);
+
+ var time = DateTime.Now;
+ var time2 = item.Execute(time);
+
+ if (value > 0)
+ Assert.True(time2 > time);
+ else if (value < 0)
+ Assert.True(time2 < time);
+ }
+
+ [Fact]
+ public void TestDefault()
+ {
+ var exp = new TimeExpression("${dt}");
+ Assert.Equal("dt", exp.Expression);
+ Assert.Equal("dt", exp.VarName);
+ Assert.Null(exp.Format);
+ Assert.Single(exp.Items);
+
+ var time = DateTime.Now;
+ var time2 = exp.Execute(time);
+ Assert.Equal(time.Date, time2);
+
+ var rs = exp.Build(time);
+ Assert.Equal(time.ToString("yyyyMMdd"), rs);
+ }
+
+ [Fact]
+ public void Test2()
+ {
+ var exp = new TimeExpression("${dt+2d}");
+ Assert.Equal("dt+2d", exp.Expression);
+ Assert.Equal("dt", exp.VarName);
+ Assert.Null(exp.Format);
+ Assert.Single(exp.Items);
+
+ var time = DateTime.Now;
+ var time2 = exp.Execute(time);
+ var time3 = time.Date.AddDays(2);
+ Assert.Equal(time3, time2);
+
+ var rs = exp.Build(time);
+ Assert.Equal(time3.ToString("yyyyMMdd"), rs);
+ }
+
+ [Fact]
+ public void Test3()
+ {
+ var exp = new TimeExpression("${dt-3H:yyMMddHH}");
+ Assert.Equal("dt-3H:yyMMddHH", exp.Expression);
+ Assert.Equal("dt", exp.VarName);
+ Assert.Equal("yyMMddHH", exp.Format);
+ Assert.Single(exp.Items);
+
+ var time = DateTime.Now;
+ var time2 = exp.Execute(time);
+ var time3 = time.Date.AddHours(time.Hour - 3);
+ Assert.Equal(time3, time2);
+
+ var rs = exp.Build(time);
+ Assert.Equal(time3.ToString("yyMMddHH"), rs);
+ }
+
+ [Fact]
+ public void Test4()
+ {
+ var exp = new TimeExpression("${dt+1M+4d:yy-MM-dd}");
+ Assert.Equal("dt+1M+4d:yy-MM-dd", exp.Expression);
+ Assert.Equal("dt", exp.VarName);
+ Assert.Equal("yy-MM-dd", exp.Format);
+ Assert.Equal(2, exp.Items.Count);
+
+ var time = DateTime.Now;
+ var time2 = exp.Execute(time);
+ var time3 = time.Date.AddDays(1 - time.Day).AddMonths(1).AddDays(4);
+ Assert.Equal(time3, time2);
+
+ var rs = exp.Build(time);
+ Assert.Equal(time3.ToString("yy-MM-dd"), rs);
+ }
+}
diff --git a/Readme.MD b/Readme.MD
index dc3693c..fbf8acc 100644
--- a/Readme.MD
+++ b/Readme.MD
@@ -20,6 +20,15 @@
使用教程:[https://newlifex.com/blood/antjob](https://newlifex.com/blood/antjob)
体验地址:[http://ant.newlifex.com](http://ant.newlifex.com/)
+# v4架构升级
+v4版本是对v3版本的重构,主要是为了解决v3版本的一些问题,以及提供更多的功能。
+v4版本亮点:
+1. []新增Http接入,由AntWeb提供调度服务,无需部署AntServer,满足轻量级项目需要。(进行中,等NewLife.Remoting提供WebsocketClient)
+2. [x]增强定时调度,支持指定Cron表达式,逐步替代Start+Step的恒定间隔定时调度
+3. []提前生成任务,提前下发给执行器,到时间后马上执行,提高任务执行时间精度。(待重新评估)
+4. []支持任务主动延迟,任务在执行中发现数据条件未满足时,可以向调度中心请求延迟一段时间后再执行,增加执行次数但不增加错误次数
+5. []扩充调度模式,常态化部署AntAgent,正式把Sql调度和C#调度加入主线,将来增加数据抽取和数据推送等多种调度模式
+
# 功能特点
AntJob的核心是**蚂蚁算法**:**把任意大数据拆分成为小块,采用蚂蚁搬家策略计算每一块!**
(蚂蚁搬家,一个馒头掉在地上,众多小蚂蚁会把馒头掰成小块小块往家里般!)
@@ -28,7 +37,7 @@ AntJob的核心是**蚂蚁算法**:**把任意大数据拆分成为小块,
2016年在中通快递某产品项目中使用该算法进行大数据实时计算,成功挑战每日1200万的订单。并进一步发展衍生成为重量级实时计算平台,集分布式计算、集群调度、配置中心、负载均衡、故障转移、跨机房冗余、作业监控告警、百亿级数据清洗、超大Redis缓存(>2T)于一身,于2019年达到每年万亿级计算量(2019年双十一日订单量破亿)。
-AntJob是开源简化版,仅提供分布式计算和集中调度能力,支持百亿级调度(需要改造)。
+AntJob是开源简化版,仅提供分布式计算和集中调度能力,支持百亿级调度。
AntJob主要功能点:
1. 作业处理器。每一个最小业务模块实现一个处理器类,用于处理这一类作业。例如同步数据表时,每张表写一个处理器类,并在调度中心注册一个作业,调度中心按照作业时间切片得到任务,然后把任务(主要包含时间区间)分派给各个计算节点上的处理器类执行。又如,每天汇总计算是一个作业,而每月汇总计算又是另一个作业;
@@ -57,6 +66,7 @@ AntJob主要功能点:
9. 支持数据库同步到HTTP;
10. 支持RocketMQ/Kafka/MQTT到数据库的反向同步;
+
# 定时调度
以下源码位于 [https://github.com/NewLifeX/AntJob/tree/master/Samples/HisAgent](https://github.com/NewLifeX/AntJob/tree/master/Samples/HisAgent)
@@ -114,14 +124,14 @@ namespace HisAgent
{
// 今天零点开始,每10秒一次
var job = Job;
- job.Start = DateTime.Today;
+ job.Time = DateTime.Today;
job.Step = 10;
}
protected override Int32 Execute(JobContext ctx)
{
// 当前任务时间
- var time = ctx.Task.Start;
+ var time = ctx.Task.Time;
WriteLog("新生命蚂蚁调度系统!当前任务时间:{0}", time);
// 成功处理数据量
@@ -194,7 +204,7 @@ public class AntSetting : Config<AntSetting>
![image.png](https://cdn.nlark.com/yuque/0/2020/png/1144030/1586439584880-ea4aa91c-460d-4893-b849-8ac8b0995311.png#align=left&display=inline&height=340&name=image.png&originHeight=680&originWidth=1075&size=127959&status=done&style=none&width=537.5)
AppID默认取本应用名,Secret由调度中心生成并下发。
调度中心默认打开自动注册AutoRegistry,任意应用登录时自动注册,省去人工配置应用账号的麻烦。
-企业内部正式场景使用时,为安全期间,建议关闭自动注册。
+企业内部正式场景使用时,为安全起见,建议关闭自动注册。
再来看看前面跑起来的日志
```sql
diff --git a/Samples/HisAgent/BuildPatient.cs b/Samples/HisAgent/BuildPatient.cs
index 1961bf8..111aa72 100644
--- a/Samples/HisAgent/BuildPatient.cs
+++ b/Samples/HisAgent/BuildPatient.cs
@@ -6,47 +6,44 @@ using HisData;
using NewLife.Security;
using XCode;
-namespace HisAgent
+namespace HisAgent;
+
+[DisplayName("生产病人")]
+[Description("定时生成一批随机病人")]
+internal class BuildPatient : Handler
{
- [DisplayName("生产病人")]
- [Description("定时生成一批随机病人")]
- internal class BuildPatient : Handler
+ public BuildPatient()
{
- public BuildPatient()
- {
- var job = Job;
- job.Start = DateTime.Today;
- job.Step = 15;
- }
+ Job.Cron = "5 1/3 * * * ?";
+ }
- protected override Int32 Execute(JobContext ctx)
- {
- // 随机造几个病人
- var count = Rand.Next(1, 9);
+ public override Int32 Execute(JobContext ctx)
+ {
+ // 随机造几个病人
+ var count = Rand.Next(1, 9);
- var list = new List<ZYBH0>();
- for (var i = 0; i < count; i++)
+ var list = new List<ZYBH0>();
+ for (var i = 0; i < count; i++)
+ {
+ var time = DateTime.Now.AddSeconds(Rand.Next(-30 * 24 * 3600, 0));
+ var time2 = time.AddSeconds(Rand.Next(3600, 10 * 24 * 3600));
+ var pi = new ZYBH0
{
- var time = DateTime.Now.AddSeconds(Rand.Next(-30 * 24 * 3600, 0));
- var time2 = time.AddSeconds(Rand.Next(3600, 10 * 24 * 3600));
- var pi = new ZYBH0
- {
- Bhid = Rand.Next(999999),
- XM = Rand.NextString(8),
- Ryrq = time,
- Cyrq = time2,
- Sfzh = Rand.NextString(18),
- FB = Rand.NextString(6),
- State = Rand.Next(8),
- Flag = Rand.Next(2),
- };
+ Bhid = Rand.Next(999999),
+ XM = Rand.NextString(8),
+ Ryrq = time,
+ Cyrq = time2,
+ Sfzh = Rand.NextString(18),
+ FB = Rand.NextString(6),
+ State = Rand.Next(8),
+ Flag = Rand.Next(2),
+ };
- list.Add(pi);
- }
- list.Insert(true);
-
- // 成功处理数据量
- return count;
+ list.Add(pi);
}
+ list.Insert(true);
+
+ // 成功处理数据量
+ return count;
}
}
\ No newline at end of file
diff --git a/Samples/HisAgent/BuildWill.cs b/Samples/HisAgent/BuildWill.cs
index 8f1a145..d0bd870 100644
--- a/Samples/HisAgent/BuildWill.cs
+++ b/Samples/HisAgent/BuildWill.cs
@@ -6,48 +6,48 @@ using HisData;
using NewLife.Security;
using XCode;
-namespace HisAgent
+namespace HisAgent;
+
+[DisplayName("生产遗嘱")]
+[Description("根据病人生成其对应的遗嘱")]
+class BuildWill : DataHandler
{
- [DisplayName("生产遗嘱")]
- [Description("根据病人生成其对应的遗嘱")]
- class BuildWill : DataHandler
+ public BuildWill()
{
- public BuildWill()
- {
- var job = Job;
- job.Start = DateTime.Today;
- job.Step = 30;
- }
+ var job = Job;
+ job.DataTime = DateTime.Today;
+ job.Step = 30;
+ job.BatchSize = 1000;
+ }
- public override Boolean Start()
- {
- // 指定要抽取数据的实体类以及时间字段
- Factory = ZYBH0.Meta.Factory;
- Field = ZYBH0._.CreateTime;
+ public override Boolean Start()
+ {
+ // 指定要抽取数据的实体类以及时间字段
+ Factory = ZYBH0.Meta.Factory;
+ Field = ZYBH0._.CreateTime;
- return base.Start();
- }
+ return base.Start();
+ }
- protected override Boolean ProcessItem(JobContext ctx, IEntity entity)
- {
- var pi = entity as ZYBH0;
+ public override Boolean ProcessItem(JobContext ctx, IEntity entity)
+ {
+ var pi = entity as ZYBH0;
- // 创建医嘱信息
- var will = new ZYBHYZ0
- {
- Bhid = pi.Bhid,
- Mgroupid = Rand.Next(9999),
+ // 创建医嘱信息
+ var will = new ZYBHYZ0
+ {
+ Bhid = pi.Bhid,
+ Mgroupid = Rand.Next(9999),
- Kyzrq = pi.Ryrq.AddHours(1),
- Tyzrq = pi.Cyrq.AddHours(-3),
- Kyzys = Rand.NextString(8),
+ Kyzrq = pi.Ryrq.AddHours(1),
+ Tyzrq = pi.Cyrq.AddHours(-3),
+ Kyzys = Rand.NextString(8),
- State = pi.State,
- };
+ State = pi.State,
+ };
- will.Insert();
+ will.Insert();
- return true;
- }
+ return true;
}
}
\ No newline at end of file
diff --git a/Samples/HisAgent/HelloJob.cs b/Samples/HisAgent/HelloJob.cs
index 14f2b82..2180d03 100644
--- a/Samples/HisAgent/HelloJob.cs
+++ b/Samples/HisAgent/HelloJob.cs
@@ -1,29 +1,33 @@
using System;
using System.ComponentModel;
using AntJob;
+using NewLife;
+using NewLife.Security;
-namespace HisAgent
+namespace HisAgent;
+
+[DisplayName("定时欢迎")]
+[Description("简单的定时任务")]
+internal class HelloJob : Handler
{
- [DisplayName("定时欢迎")]
- [Description("简单的定时任务")]
- internal class HelloJob : Handler
+ public HelloJob()
{
- public HelloJob()
- {
- // 今天零点开始,每10秒一次
- var job = Job;
- job.Start = DateTime.Today;
- job.Step = 10;
- }
+ Job.Cron = "7/30 * * * * ?";
+ }
+
+ public override Int32 Execute(JobContext ctx)
+ {
+ using var span = Tracer?.NewSpan("HelloJob", ctx.Task.DataTime);
+
+ // 当前任务时间
+ var time = ctx.Task.DataTime;
+ WriteLog("新生命蚂蚁调度系统!当前任务时间:{0}", time);
+ if (!ctx.Task.Data.IsNullOrEmpty()) WriteLog("数据:{0}", ctx.Task.Data);
- protected override Int32 Execute(JobContext ctx)
- {
- // 当前任务时间
- var time = ctx.Task.Start;
- WriteLog("新生命蚂蚁调度系统!当前任务时间:{0}", time);
+ //// 一定几率抛出异常
+ //if (Rand.Next(2) == 0) throw new Exception("Error");
- // 成功处理数据量
- return 1;
- }
+ // 成功处理数据量
+ return 1;
}
}
\ No newline at end of file
diff --git a/Samples/HisAgent/HisAgent.csproj b/Samples/HisAgent/HisAgent.csproj
index b146300..da4a4e6 100644
--- a/Samples/HisAgent/HisAgent.csproj
+++ b/Samples/HisAgent/HisAgent.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk.Web">
+<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -10,10 +10,6 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="NewLife.Stardust.Extensions" Version="2.9.2024.402" />
- </ItemGroup>
-
- <ItemGroup>
<ProjectReference Include="..\..\AntJob.Extensions\AntJob.Extensions.csproj" />
<ProjectReference Include="..\..\AntJob\AntJob.csproj" />
<ProjectReference Include="..\HisData\HisData.csproj" />
diff --git a/Samples/HisAgent/JobHost.cs b/Samples/HisAgent/JobHost.cs
index fc5724b..c723e59 100644
--- a/Samples/HisAgent/JobHost.cs
+++ b/Samples/HisAgent/JobHost.cs
@@ -2,9 +2,9 @@
using System.Threading;
using System.Threading.Tasks;
using AntJob;
-using AntJob.Providers;
-using Microsoft.Extensions.Hosting;
using NewLife;
+using NewLife.Log;
+using NewLife.Model;
namespace HisAgent;
@@ -12,36 +12,32 @@ public class JobHost : BackgroundService
{
private Scheduler _scheduler;
private readonly IServiceProvider _serviceProvider;
+ private readonly AntSetting _setting;
- public JobHost(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;
+ public JobHost(IServiceProvider serviceProvider, AntSetting setting)
+ {
+ _serviceProvider = serviceProvider;
+ _setting = setting;
+ }
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
- var set = AntSetting.Current;
-
// 实例化调度器
var scheduler = new Scheduler
{
ServiceProvider = _serviceProvider,
-
- //// 使用分布式调度引擎替换默认的本地文件调度
- //Provider = new NetworkJobProvider
- //{
- // Server = set.Server,
- // AppID = set.AppID,
- // Secret = set.Secret,
- // Debug = false
- //}
+ Log = XTrace.Log,
};
- scheduler.Join(set.Server, set.AppID, set.Secret, set.Debug);
+ scheduler.Join(_setting);
// 添加作业
scheduler.AddHandler<HelloJob>();
-
+ scheduler.AddHandler<BuildPatient>();
+ scheduler.AddHandler<BuildWill>();
// 启动调度引擎,调度器内部多线程处理
- scheduler.Start();
+ scheduler.StartAsync();
_scheduler = scheduler;
return Task.CompletedTask;
diff --git a/Samples/HisAgent/Program.cs b/Samples/HisAgent/Program.cs
index 823b0cd..bceddee 100644
--- a/Samples/HisAgent/Program.cs
+++ b/Samples/HisAgent/Program.cs
@@ -1,33 +1,20 @@
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
+using AntJob;
+using HisAgent;
using NewLife.Log;
+using NewLife.Model;
+using Stardust;
-namespace HisAgent;
+// 启用控制台日志,拦截所有异常
+XTrace.UseConsole();
-class Program
-{
- static void Main(string[] args)
- {
- XTrace.UseConsole();
+var services = ObjectContainer.Current;
+services.AddStardust();
- CreateHostBuilder(args).Build().Run();
- }
+services.AddSingleton(AntSetting.Current);
- /// <summary></summary>
- /// <param name="args"></param>
- /// <returns></returns>
- public static IHostBuilder CreateHostBuilder(string[] args) =>
- Host.CreateDefaultBuilder(args)
- .ConfigureServices((hostContext, services) => ConfigureServices(services));
+// 友好退出
+var host = services.BuildHost();
- /// <summary></summary>
- /// <param name="hostBuilderContext"></param>
- /// <param name="services"></param>
- public static void ConfigureServices(IServiceCollection services)
- {
- services.AddStardust();
+host.Add<JobHost>();
- // 添加后台调度服务
- services.AddHostedService<JobHost>();
- }
-}
\ No newline at end of file
+await host.RunAsync();
\ No newline at end of file
diff --git a/Samples/HisData/HisData.csproj b/Samples/HisData/HisData.csproj
index cef75d8..c0297b2 100644
--- a/Samples/HisData/HisData.csproj
+++ b/Samples/HisData/HisData.csproj
@@ -16,7 +16,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="NewLife.XCode" Version="11.11.2024.402" />
+ <PackageReference Include="NewLife.XCode" Version="11.16.2024.1005" />
</ItemGroup>
<ItemGroup>
diff --git a/Samples/HisWeb/HisWeb.csproj b/Samples/HisWeb/HisWeb.csproj
index 6c2207c..9fffe89 100644
--- a/Samples/HisWeb/HisWeb.csproj
+++ b/Samples/HisWeb/HisWeb.csproj
@@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="NewLife.Cube.Core" Version="6.1.2024.403" />
+ <PackageReference Include="NewLife.Cube.Core" Version="6.1.2024.1005" />
</ItemGroup>
<ItemGroup>