新聞中心
先入為主,Zookeeper 我就不深入了,整個(gè)10.1 在家基本除了看電影就是看 Quartz 了。

專注于為中小企業(yè)提供網(wǎng)站設(shè)計(jì)、成都網(wǎng)站制作服務(wù),電腦端+手機(jī)端+微信端的三站合一,更高效的管理,為中小企業(yè)嘉定免費(fèi)做網(wǎng)站提供優(yōu)質(zhì)的服務(wù)。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動了上千企業(yè)的穩(wěn)健成長,幫助中小企業(yè)通過網(wǎng)站建設(shè)實(shí)現(xiàn)規(guī)模擴(kuò)充和轉(zhuǎn)變。
Quartz.net 提供了 Remoting 方式,Remoting 我08年的時(shí)候,寫過個(gè)小程序,讓一臺服務(wù)器通知另外一臺服務(wù)器去執(zhí)行一些任務(wù),簡單的應(yīng)用,除了這些,在沒有深入過了。
看一下結(jié)構(gòu):
Jobs 下是任務(wù)
Listener 沒有寫好,不顯擺了。
QM.Server 是 Quartz 的調(diào)度服務(wù)
QM.Shell 是一個(gè)管理工具
我試想把管理工具放到本地來,即服務(wù)在服務(wù)器上,管理工具在本地,這樣就方便管理,但是除非本地能找到調(diào)度服務(wù)所用的所有的DLL,否則就會因?yàn)闊o法加載類型XXX而無法進(jìn)行下去。
有了這個(gè)問題,那么只好管理工具和調(diào)度服務(wù)放一起了。
即然要把管理工具和服務(wù)放一起,那就不必把DLL COPY的到處都是,但是需要做一些特殊處理
下面是生成的目錄結(jié)構(gòu):
XXXXXX 管理工具目錄,
XXXXXX\Service 調(diào)度服務(wù)程序集存放在這里
XXXXXX\Service\Jobs 任務(wù)程序集存放在這里
要讓管理和調(diào)度服務(wù)都能找得到 任務(wù)程序集,需要改一下 app.config
管理工具的 app.config
.......
調(diào)度服務(wù)的 app.config
......
這樣一來,把新寫好的任務(wù)程序集放到 XXXXX\Service\Jobs 下,管理工具和調(diào)度服務(wù)都可以找得到了。
QM.Server 用到的東西:
Quartz.Net 2.2.4.400
log4net 1.2.10
Common.Logging
Common.Logging.log4net 由于 log4net 選用的是 1.2.10 ,所以這個(gè)只能用 2.0
Topshelf
列出來這些是因?yàn)槲矣?NuGet 搜索安裝的 Common.Logging.log4net ,程序一運(yùn)行就退出。手動指定為 2.0 的 Common.Logging.log4net 才沒有問題。
調(diào)度服務(wù)我用的是 SqlCe 4.0 來存任務(wù)信息,所以需要安裝 SqlCe 4.0 的驅(qū)動, 可以從這里下載:
http://www.microsoft.com/en-us/download/details.aspx?id=17876
如果想指定其它數(shù)據(jù)庫,可以修改 quartz.config
quartz.jobStore.type = Quartz.Impl.AdoJobStore.JobStoreTX, Quartz quartz.jobStore.driverDelegateType = Quartz.Impl.AdoJobStore.StdAdoDelegate, Quartz quartz.jobStore.dataSource = ds quartz.dataSource.ds.connectionString = Data Source=QUARTZ.sdf;Persist Security Info=False; quartz.dataSource.ds.provider = SqlServerCe-400
因?yàn)楣芾砉ぞ呤腔?Quartz REMOTING 的,所以要在 quartz.coonfig 中指定以下配置:
quartz.scheduler.exporter.type = Quartz.Simpl.RemotingSchedulerExporter, Quartz quartz.scheduler.exporter.port = 5555 quartz.scheduler.exporter.bindName = TestScheduler quartz.scheduler.exporter.channelType = tcp quartz.scheduler.exporter.channelName = httpQuartz
這里我指定的端口號是 5555, bindName 是 TestScheduler, 這兩個(gè)東西要用在管理工具上。
Listener , Listener 不能通過 Remoting 來管理,因?yàn)?Not Support. 我嘗試做一個(gè)通UDP來廣播 Listener ,不知道哪里沒有寫好,就是收不到數(shù)據(jù)。沒有做好,我就不多廢話了。
管理工具用到的東西:
Quartz
Caliburn.Micro
看到 Caliburn 就知道這個(gè)管理工具是用 WPF 寫的了!沒什么講的,上圖看看:
連接界面,主機(jī)即調(diào)度服務(wù)所在的IP
端口和 Scheduler 是上面所講的配置中的 port 和 bindName
觸發(fā)器/任務(wù)列表,可以用來停止/啟動/編輯/刪除指定的任務(wù)。
日歷編輯界面,目前只實(shí)現(xiàn)了 HolidayCalendar 的編輯,Holiday 是用來指定任務(wù)在哪些日期不執(zhí)行的。
這個(gè)功能 Cron 表達(dá)式無法做到,其它的基本都可以由 Cron 來做,所以目前只實(shí)現(xiàn)了 HolidayCalendar 的編輯。
這個(gè)是日歷列表界面,右邊會跟據(jù)不同的日歷類型顯示日歷詳細(xì),只是目前只實(shí)現(xiàn)了 HolidayCalendar, 所以右邊只有這一個(gè)界面。
任務(wù)編輯界面,觸發(fā)器類型編輯器目前只實(shí)現(xiàn)了 Simple 和 Cron 類型的。
概覽。
源碼下載:
http://files.cnblogs.com/xling/QuartzJobManager.7z
要運(yùn)行示例,需要運(yùn)行 QM.Shell\bin\Debug\Service\QM.Server.exe 然后在運(yùn)行 QM.Shell.exe 。
Quartz.NET 的API還是挺簡單的,還有許多細(xì)小功能沒有添加上。
另外還有一個(gè)分布式的,還沒有看相關(guān)資料,沒有處理。
#p#
界面具體變化如下:
任務(wù)參數(shù)可視化
如上圖所示, 在管理任務(wù)的界面上就可以知道這個(gè)任務(wù)需哪些參數(shù)/類型 及 參數(shù)的說明.
實(shí)現(xiàn)方式, 在 Job 上添加 特性 : ParameterTypeAttribute
1 namespace JobA {
2 [ParameterType(typeof(Parameter))]
3 public class Job : IJob {
4
5 public static ILog Log = LogManager.GetLogger(typeof(Job));
6 public void Execute(IJobExecutionContext context) {
7 var dataMap = context.JobDetail.JobDataMap;
8 //if (dataMap.ContainsKey("int")) {
9 // var pInt = dataMap.GetIntValue("int");
10 // Console.WriteLine("1 JobA Parameter {0}", pInt);
11 //} else {
12 // Log.Error("缺少參數(shù) int, 未執(zhí)行");
13 // throw new JobExecutionException("缺少參數(shù)");
14 //}
15
16 var p = dataMap.Parse();
17 Console.WriteLine("{0}\t{1}\t{2}\t{3}", p.PDateTime, p.PDecimal, p.PInt, p.PNullableInt);
18
19
20 Thread.Sleep(TimeSpan.FromMinutes(3));
21 }
22 }
23 } 取參數(shù)直接調(diào)用 dataMap.Parse
Parse 方法在: QM.Common. DatamapParser 中定義.
相比原始的從 DataMap 中用 key / value 方法取參數(shù), 這種處理方式的好處不言而喻.
但是也有缺點(diǎn), DataMap 支持任何可序列化的類型,
而用這種方法只支持
string, decimal, long, int, single, double, DateTime, DateTimeOffset, TimeSpan , bool, char 這些類型. (沒有做更深一步的處理, 有興趣的,可以嘗試自己去實(shí)現(xiàn).)
每個(gè)任務(wù)獨(dú)立的應(yīng)用程序域
試想一下插件式開發(fā), 如果你做的插件需要N個(gè)第三方DLL, 而這些DLL并沒有引用到主項(xiàng)目上, 怎么辦呢? 一堆的 FileLoadException, FileNotFoundException 等錯(cuò)誤, 想想都頭疼.
如果你開發(fā)的插件想擁有自己的配置文件, 又該怎么辦呢? 自己實(shí)現(xiàn)一個(gè)配置文件讀取解析? ini ? xml ? 頭疼吧.
針對上面的問題, 在這里的最佳解決辦法是 : 獨(dú)立的應(yīng)用程序域.
這個(gè)要從 IScheduler.JobFactory 說起.
在QM.Server.QMServer 的構(gòu)造方法中, 指定 Schedule.JobFactory 為 IsolatedJobFactory
IsolatedJobFactory 的定義:
1 public class IsolatedJobFactory : IJobFactory {
2
3 public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) {
4 return NewJob(bundle.JobDetail.JobType);
5 }
6
7 private IJob NewJob(Type jobType) {
8 return new IsolatedJob(jobType);
9 }
10
11 public void ReturnJob(IJob job) {
12 IDisposable disposable = job as IDisposable;
13 if (disposable != null) {
14 disposable.Dispose();
15 }
16 }
17 }從 NewJob 方法上可以看出, 實(shí)例出來的 Job 并不是最終要執(zhí)行的 Job, 而是 IsolatedJob 的實(shí)例, 它類似中間人的身份.
IsolatedJob 實(shí)現(xiàn)了 IInterruptableJob 接口, 為中斷執(zhí)行中的任務(wù)埋下伏筆.
在 IsolatedJob 的構(gòu)造方法中, 通過 IsolateDomainLoader 新建一個(gè)應(yīng)用程序域:
IsolatedDomainLoader 的構(gòu)造函數(shù):
1 public IsolateDomainLoader(string path, string configFileName = "") {
2 AppDomainSetup setup = new AppDomainSetup();
3 setup.ApplicationName = "IsolateDomainLoader";
4 setup.ApplicationBase = path;
5 setup.DynamicBase = path;
6 setup.PrivateBinPath = path;
7 setup.CachePath = setup.ApplicationBase;
8 setup.ShadowCopyFiles = "true";
9 setup.ShadowCopyDirectories = setup.ApplicationBase;
10 if (!string.IsNullOrWhiteSpace(configFileName)) {
11 setup.ConfigurationFile = configFileName;
12 setup.ConfigurationFile = Path.Combine(path, configFileName);
13 }
14 this.Domain = AppDomain.CreateDomain("ApplicationLoaderDomain", null, setup);
15 }參數(shù) path 即最終要執(zhí)行的 job 所在的 dll 的路徑.
configFileName 即獨(dú)立的配置文件名稱.
這樣一來, 一個(gè) job 一個(gè)文件夾, 文件夾內(nèi)放置這個(gè) job 相關(guān)的DLL和配置文件, 和主程序完全隔離開來.
上面說 IsolatedJob 是個(gè)中間人, 這里解釋一下:
1, IsolatedJobFactory 的 NewJob 方法返回的是 IsolatedJob 的實(shí)例, 而不是最終要執(zhí)行的 Job.
2, 在 IsolatedJob 中, 會通過獨(dú)立的應(yīng)用程序域 實(shí)例一個(gè)最終要執(zhí)行的 Job 的遠(yuǎn)程對象(通過 RemoteObject).
3, 當(dāng)中間人的 Execute 方法被調(diào)用時(shí), 會調(diào)用遠(yuǎn)程 Job 對象的 Execute 方法.
4, Interrupt 方法同理.
遠(yuǎn)程對象續(xù)約
因?yàn)楠?dú)立的應(yīng)用程序域用到了遠(yuǎn)程對象: MarshalByRefObject, 因此涉及到了遠(yuǎn)程對象的租約過期及續(xù)租的問題.
遠(yuǎn)程對象的租約默認(rèn)為 5 分鐘, 可以重寫 InitializeLifetimeService 方法來修改租約的有效期. 但是一個(gè) Job 不確定要執(zhí)行多長時(shí)間, 修改租約有效期不是很合適, 所以這里是通過續(xù)約的方式來處理租約過期的問題.
本人對租約了解不多, 不多嘴.感興趣的話,可參見源碼:
QM.RemoteLoader.RemoteObjectSponsor 類
和 QM.RemoteLoader.IsolateDomainLoader類的 GetObject 方法.
立即中斷正在執(zhí)行的任務(wù)
這個(gè)命題是有條件的, 即: 任務(wù)必須實(shí)現(xiàn): IInterruptableJob 接口.
一般一個(gè)任務(wù)要執(zhí)行很長時(shí)間, 如果不給個(gè)中斷的接口, 那就只能關(guān)閉服務(wù)或等任務(wù)執(zhí)行完畢了.
實(shí)現(xiàn)了這個(gè)接口,在配合 CancellationToken.ThrowIfCancellationRequested 方法就可以中斷當(dāng)前執(zhí)行的任務(wù)了(別告訴我,你的任務(wù)是單線程的).
卸載域
任務(wù)執(zhí)行完成后, 會將關(guān)聯(lián)的 IsolatedJob對象釋放, 在 IsolatedJob 的 Dispose 方法中,會把IsolateDomainLoader 對象釋放,IsolateDomainLoader 釋放的時(shí)候, 會把關(guān)聯(lián)的子應(yīng)用程序域卸載.
所以, 如果如果你的任務(wù)是多線程的, 請?jiān)诰€程遠(yuǎn)行完之前, 進(jìn)行阻塞.
自定義Job的基類
目前, 如果自定義的 Job 的基類在第三方DLL中, 而且第三方DLL未引用到QM.Server 項(xiàng)目中, 并且不在 QM.Server\Jobs 目錄下, 會報(bào):
未能加載文件或程序集 XXX 或它的某一個(gè)依賴項(xiàng)。系統(tǒng)找不到指定的文件。
解決辦法有兩種:
1, 將缺少的DLL放到Jobs 目錄下.
2, 將缺少的DLL添加引用到 QM.Server 中.
注意, 該限制只針對 Job 的基類. 除基類使用外的第三方DLL不需要這樣做, 在JOB上引用就是了.
放上一段不用的, 可終止的 任務(wù)示例代碼 給你做參考
- [ParameterType(typeof(FetcherParameter))]
- public class ScheduleFetcherJob : IInterruptableJob, IDisposable {
- private CancellationTokenSource CTS = new CancellationTokenSource();
- private long JobID = DateTime.Now.Ticks;
- public void Execute(IJobExecutionContext context) {
- var par = context.JobDetail.JobDataMap.Parse
(); - this.CTS.Token.Register(() => {
- Console.WriteLine("正在嘗試終止當(dāng)前任務(wù)");
- });
- this.Execute(par);
- }
- private string GetUrl(string org, string dest) {
- return string.Format("http://www.soushipping.com/shipping/{0}/{1}/{2}",
- org, dest,
- DateTime.Now.ToString("yyyy-MM-dd"));
- }
- private void Execute(FetcherParameter par) {
- IFetcher
cityFetcher = new OrginCityFetcher(); - var orgCities = cityFetcher.GetDatasByUrl(cityFetcher.InitUrl);
- cityFetcher = new DestCityFetcher();
- var destCities = cityFetcher.GetDatasByUrl(cityFetcher.InitUrl);
- Console.WriteLine("找到 {0} 條始發(fā)地, {1} 條目的地", orgCities.Count(), destCities.Count());
- var limitScd = new LimitedConcurrencyLevelTaskScheduler(par.MaxThread);
- var factory = new TaskFactory(limitScd);
- List
tasks = new List (); - foreach (var oc in orgCities.ToList()) {
- foreach (var dc in destCities.ToList()) {
- //注意下面這句的參數(shù) t, 如果帶這個(gè)參數(shù), IsCanceled 永遠(yuǎn)都為 false
- //var task = Task.Factory.StartNew((t) => {
- var task = factory.StartNew(() => {
- this.CTS.Token.ThrowIfCancellationRequested();
- var url = this.GetUrl(oc, dc);
- var fetcher = new ScheduleFetcher(url);
- fetcher.PageFetchCompleted += fetcher_PageFetchCompleted;
- fetcher.DownloadCompleted += fetcher_DownloadCompleted;
- fetcher.Fetch();
- fetcher = null;
- }, this.CTS.Token)
- .ContinueWith(t => {
- //var completed = tasks.Where(tt => tt.Status == TaskStatus.RanToCompletion).Count();
- //Console.WriteLine("{0}\t已完成:{1}", DateTime.Now.ToString("yyyy/MM/dd"), completed);
- var arr = tasks.GroupBy(tt => tt.Status).Select(g => string.Format("{0}:{1}", g.Key, g.Count()));
- Console.WriteLine("{0}\t{1}", DateTime.Now.ToString("MM/dd HH:mm:ss"), string.Join(" ", arr));
- t.Dispose();
- });//
- //, TaskContinuationOptions.OnlyOnRanToCompletion)
- //.ContinueWith(t => {
- // //Console.WriteLine("正在取消");
- // t.Dispose();
- //}, TaskContinuationOptions.OnlyOnCanceled).ContinueWith(t => {
- // Console.WriteLine("發(fā)生錯(cuò)誤");
- // t.Dispose();
- //}, TaskContinuationOptions.OnlyOnFaulted);
- tasks.Add(task);
- }
- }
- try {
- Task.WaitAll(tasks.ToArray());
- } catch (AggregateException ex) {
- ex.Handle(er => er is TaskCanceledException);
- }
- Console.WriteLine("任務(wù)完成");
- }
- #region
- //private void Execute2(FetcherParameter par) {
- // IFetcher
cityFetcher = new OrginCityFetcher(); - // var orgCities = cityFetcher.GetDatasByUrl(cityFetcher.InitUrl);
- // cityFetcher = new DestCityFetcher();
- // var destCities = cityFetcher.GetDatasByUrl(cityFetcher.InitUrl);
- // Console.WriteLine("找到 {0} 條始發(fā)地, {1} 條目的地", orgCities.Count(), destCities.Count());
- // var urls = orgCities.SelectMany(o => destCities.Select(d => this.GetUrl(o, d)));
- // var opts = new ParallelOptions() {
- // MaxDegreeOfParallelism = par.MaxThread
- // };
- // var total = urls.Count();
- // object lockObj = new object();
- // //int sum = 0;
- // Parallel.ForEach(urls, opts,
- // (url) => {
- // var fetcher = new ScheduleFetcher(url);
- // fetcher.PageFetchCompleted += fetcher_PageFetchCompleted;
- // fetcher.DownloadCompleted += fetcher_DownloadCompleted;
- // fetcher.Fetch();
- // fetcher = null;
- // lock (lockObj) {
- // total--;
- // Console.WriteLine(total);
- // }
- // }
- // );
- //}
- #endregion
- private void fetcher_DownloadCompleted(object sender, DownloadArgs e) {
- if (e.ExceptionStatus.HasValue) {
- Console.WriteLine("{0}\t請求地址: {1} 時(shí),發(fā)生異常 {2}, 請檢查網(wǎng)絡(luò)環(huán)境.", DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"), e.Url, e.ExceptionStatus);
- }
- }
- private List
Datas = new List (); - private object lockObj = new object();
- void fetcher_PageFetchCompleted(object sender, FetchArgs
e) { - var datas = e.Datas.Distinct(d => d.UNQTAG);
- lock (lockObj) {
- this.Datas.AddRange(datas);
- if (this.Datas.Count > 100) {
- var tmp = new DIRTY_SCHEDULE[this.Datas.Count];
- this.Datas.CopyTo(tmp);
- this.Datas = new List
(); - //不是放入線程池, 而是立即執(zhí)行的線程
- var tr = new Thread(new ParameterizedThreadStart(this.SaveDatas));
- tr.Start(tmp);
- }
- }
- }
- private void SaveDatas(object state) {
- IEnumerable
datas = (IEnumerable )state; - var biz = new Biz.DirtyScheduleBiz();
- biz.SaveDirtySchedule(datas, this.JobID);
- }
- public void Interrupt() {
- this.CTS.Cancel();
- }
- ~ScheduleFetcherJob() {
- Dispose(false);
- }
- public void Dispose() {
- this.Dispose(true);
- GC.SuppressFinalize(this);
- }
- protected virtual void Dispose(bool disposing) {
- if (disposing) {
- if (this.CTS != null)
- this.CTS.Dispose();
- Console.WriteLine("Job Disposed");
- }
- }
- }
最后, 源碼下載
https://github.com/gruan01/QM
謝謝圍觀, 新年快樂!
----------------------------
題外: 大年初二, 我手一抖, 把斷斷續(xù)續(xù)寫了快一年的東西給誤刪了!誤刪了啊!
用 360 的數(shù)據(jù)恢復(fù)功能, 沒錯(cuò), 是 360, 找出的文件, 我哭了, 數(shù)據(jù)庫(SQLCE) 恢復(fù)出來的文件損壞, 用SQLCE的修復(fù)工具修復(fù), 是個(gè)空庫! 也就是說, 恢復(fù)出來的文件就是個(gè)屁!跟本就沒有恢復(fù)出來!
EXCEL 文件也一樣, 打不開!
更糟糕的是, 我沒有驗(yàn)證, 恢復(fù)之后就直接蓋到原來的位置上了!
淚奔啊, 大過年的, 我就忙著干這個(gè)去了!
分享名稱:基于Quqrtz.NET做的任務(wù)調(diào)度管理工具
本文網(wǎng)址:http://www.dlmjj.cn/article/dpoheeh.html


咨詢
建站咨詢
