Free cookie consent management tool by TermsFeed Policy Generator

source: trunk/sources/HeuristicLab.Clients.Hive/3.3/RefreshableJob.cs @ 8871

Last change on this file since 8871 was 8871, checked in by ascheibe, 12 years ago

#1950

  • fixed a bug where runs were downloaded multiple times
  • fixed a bug where "Refresh automatically" wasn't disabled when the job was finished
  • added a locker around the code that integrates downloaded optimizers as in rare cases collections were modified by multiple threads which lead to an exception
File size: 22.4 KB
RevLine 
[6976]1#region License Information
2/* HeuristicLab
[7259]3 * Copyright (C) 2002-2012 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
[6976]4 *
5 * This file is part of HeuristicLab.
6 *
7 * HeuristicLab is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * HeuristicLab is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
19 */
20#endregion
21
22using System;
23using System.Collections.Generic;
24using System.ComponentModel;
25using System.Drawing;
26using System.Linq;
27using HeuristicLab.Collections;
28using HeuristicLab.Common;
29using HeuristicLab.Core;
[7582]30using HeuristicLab.MainForm;
[6976]31
32namespace HeuristicLab.Clients.Hive {
[8156]33  public class RefreshableJob : IHiveItem, IDeepCloneable, IContent, IComparable<RefreshableJob> {
[6976]34    private JobResultPoller jobResultPoller;
35    private ConcurrentTaskDownloader<ItemTask> jobDownloader;
[8871]36    private object locker = new object();
37    private object downloadFinishedLocker = new object();
[6976]38
[8156]39    public bool IsProgressing { get; set; }
40
[6976]41    private Job job;
42    public Job Job {
43      get { return job; }
44      set {
45        if (value != job) {
46          if (value == null)
47            throw new ArgumentNullException();
48
49          if (job != null) DergisterJobEvents();
50          job = value;
51          if (job != null) {
52            RegisterJobEvents();
53            job_PropertyChanged(job, new PropertyChangedEventArgs("Id"));
54          }
55          OnJobChanged();
56          OnToStringChanged(this, EventArgs.Empty);
[7059]57          job_ItemImageChanged(this, EventArgs.Empty);
[6976]58        }
59      }
60    }
61
62    private ItemCollection<HiveTask> hiveTasks;
63    public ItemCollection<HiveTask> HiveTasks {
64      get { return hiveTasks; }
65      set {
66        if (hiveTasks != value) {
67          if (hiveTasks != null) DeregisterHiveJobsEvents();
68          hiveTasks = value;
69          if (hiveTasks != null) RegisterHiveJobsEvents();
70          OnHiveTasksChanged();
71        }
72      }
73    }
74
75    private ExecutionState executionState;
76    public ExecutionState ExecutionState {
77      get { return executionState; }
78      internal set {
79        if (executionState != value) {
80          executionState = value;
81          OnExecutionStateChanged();
82        }
83      }
84    }
85
86    private TimeSpan executionTime;
87    public TimeSpan ExecutionTime {
88      get { return executionTime; }
89      internal set {
90        if (executionTime != value) {
91          executionTime = value;
92          OnExecutionTimeChanged();
93        }
94      }
95    }
96
97    private bool refreshAutomatically;
98    public bool RefreshAutomatically {
99      get { return refreshAutomatically; }
100      set {
101        lock (locker) {
102          if (refreshAutomatically != value) {
103            refreshAutomatically = value;
104            OnRefreshAutomaticallyChanged();
105          }
106          if (RefreshAutomatically) {
107            if (this.HiveTasks != null && this.HiveTasks.Count > 0 && (jobResultPoller == null || !jobResultPoller.IsPolling)) {
108              StartResultPolling();
109            }
110          } else {
111            StopResultPolling();
112          }
113        }
114      }
115    }
116
117    // indicates if download button is enabled
118    private bool isDownloadable = true;
119    public bool IsDownloadable {
120      get { return isDownloadable; }
121      set {
122        if (value != isDownloadable) {
123          isDownloadable = value;
124          OnIsDownloadableChanged();
125        }
126      }
127    }
128
129    // if true, all control buttons should be enabled. otherwise disabled
130    private bool isControllable = true;
131    public bool IsControllable {
132      get { return isControllable; }
133      private set {
134        if (value != isControllable) {
135          isControllable = value;
136          OnIsControllableChanged();
137          if (this.hiveTasks != null) {
138            foreach (var hiveJob in this.hiveTasks) {
139              hiveJob.IsControllable = value;
140            }
141          }
142        }
143      }
144    }
145
146    // indicates if a user is allowed to share this experiment
147    private bool isSharable = true;
148    public bool IsSharable {
149      get { return isSharable; }
150      private set {
151        if (value != isSharable) {
152          isSharable = value;
153          OnIsSharableChanged();
154        }
155      }
156    }
157
158    // may execute jobs with privileged permissions on slaves
159    private bool isAllowedPrivileged = true;
160    public bool IsAllowedPrivileged {
161      get { return isAllowedPrivileged; }
162      set {
163        if (value != isAllowedPrivileged) {
164          isAllowedPrivileged = value;
165          OnIsAllowedPrivilegedChanged();
166        }
167      }
168    }
169
[8165]170    private Progress progress;
171    public Progress Progress {
[6976]172      get { return progress; }
[8156]173      set {
174        this.progress = value;
175        OnIsProgressingChanged();
176      }
[6976]177    }
178
[8156]179
[6976]180    private ThreadSafeLog log;
181    public ILog Log {
182      get { return log; }
183    }
184
185    public StateLogListList StateLogList {
186      get { return new StateLogListList(this.GetAllHiveTasks().Select(x => x.StateLog)); }
187    }
188
189    #region Constructors and Cloning
190    public RefreshableJob() {
191      this.refreshAutomatically = true;
192      this.Job = new Job();
193      this.log = new ThreadSafeLog();
[7020]194      this.jobDownloader = new ConcurrentTaskDownloader<ItemTask>(Settings.Default.MaxParallelDownloads, Settings.Default.MaxParallelDownloads);
[6976]195      this.jobDownloader.ExceptionOccured += new EventHandler<EventArgs<Exception>>(jobDownloader_ExceptionOccured);
196      this.HiveTasks = new ItemCollection<HiveTask>();
197    }
198    public RefreshableJob(Job hiveExperiment) {
199      this.refreshAutomatically = true;
200      this.Job = hiveExperiment;
201      this.log = new ThreadSafeLog();
[7020]202      this.jobDownloader = new ConcurrentTaskDownloader<ItemTask>(Settings.Default.MaxParallelDownloads, Settings.Default.MaxParallelDownloads);
[6976]203      this.jobDownloader.ExceptionOccured += new EventHandler<EventArgs<Exception>>(jobDownloader_ExceptionOccured);
204      this.HiveTasks = new ItemCollection<HiveTask>();
205    }
206    protected RefreshableJob(RefreshableJob original, Cloner cloner) {
207      cloner.RegisterClonedObject(original, this);
[6994]208      this.Job = cloner.Clone(original.Job);
[6976]209      this.IsControllable = original.IsControllable;
210      this.log = cloner.Clone(original.log);
211      this.RefreshAutomatically = false; // do not start results polling automatically
[7020]212      this.jobDownloader = new ConcurrentTaskDownloader<ItemTask>(Settings.Default.MaxParallelDownloads, Settings.Default.MaxParallelDownloads);
[6976]213      this.jobDownloader.ExceptionOccured += new EventHandler<EventArgs<Exception>>(jobDownloader_ExceptionOccured);
214      this.HiveTasks = cloner.Clone(original.HiveTasks);
215      this.ExecutionTime = original.ExecutionTime;
216      this.ExecutionState = original.ExecutionState;
217    }
218    public IDeepCloneable Clone(Cloner cloner) {
219      return new RefreshableJob(this, cloner);
220    }
221    public object Clone() {
222      return this.Clone(new Cloner());
223    }
224    #endregion
225
226    #region JobResultPoller Events
227    public void StartResultPolling() {
228      if (jobResultPoller == null) {
[7020]229        jobResultPoller = new JobResultPoller(job.Id, Settings.Default.ResultPollingInterval);
[6976]230        RegisterResultPollingEvents();
[8869]231        jobResultPoller.AutoResumeOnException = false;
[6976]232      }
233
234      if (!jobResultPoller.IsPolling) {
235        jobResultPoller.Start();
236      }
237    }
238
239    public void StopResultPolling() {
240      if (jobResultPoller != null && jobResultPoller.IsPolling) {
241        jobResultPoller.Stop();
242      }
243    }
244
245    private void RegisterResultPollingEvents() {
246      jobResultPoller.ExceptionOccured += new EventHandler<EventArgs<Exception>>(jobResultPoller_ExceptionOccured);
247      jobResultPoller.JobResultsReceived += new EventHandler<EventArgs<IEnumerable<LightweightTask>>>(jobResultPoller_JobResultReceived);
248      jobResultPoller.IsPollingChanged += new EventHandler(jobResultPoller_IsPollingChanged);
249    }
250    private void DeregisterResultPollingEvents() {
251      jobResultPoller.ExceptionOccured -= new EventHandler<EventArgs<Exception>>(jobResultPoller_ExceptionOccured);
252      jobResultPoller.JobResultsReceived -= new EventHandler<EventArgs<IEnumerable<LightweightTask>>>(jobResultPoller_JobResultReceived);
253      jobResultPoller.IsPollingChanged -= new EventHandler(jobResultPoller_IsPollingChanged);
254    }
255    private void jobResultPoller_IsPollingChanged(object sender, EventArgs e) {
256      if (this.refreshAutomatically != jobResultPoller.IsPolling) {
257        this.refreshAutomatically = jobResultPoller.IsPolling;
258        OnRefreshAutomaticallyChanged();
259      }
260    }
[8871]261
[6976]262    private void jobResultPoller_JobResultReceived(object sender, EventArgs<IEnumerable<LightweightTask>> e) {
263      foreach (LightweightTask lightweightTask in e.Value) {
[7218]264        HiveTask hiveTask = GetHiveTaskById(lightweightTask.Id);
[6976]265        if (hiveTask != null) {
266          // lastJobDataUpdate equals DateTime.MinValue right after it was uploaded. When the first results are polled, this value is updated
267          if (hiveTask.Task.State == TaskState.Offline && lightweightTask.State != TaskState.Finished && lightweightTask.State != TaskState.Failed && lightweightTask.State != TaskState.Aborted) {
268            hiveTask.Task.LastTaskDataUpdate = lightweightTask.LastTaskDataUpdate;
269          }
270
271          hiveTask.UpdateFromLightweightJob(lightweightTask);
272
273          if (!hiveTask.IsFinishedTaskDownloaded && !hiveTask.IsDownloading && hiveTask.Task.LastTaskDataUpdate < lightweightTask.LastTaskDataUpdate) {
274            log.LogMessage(string.Format("Downloading task {0}", lightweightTask.Id));
275            hiveTask.IsDownloading = true;
[7115]276            jobDownloader.DownloadTaskData(hiveTask.Task, (localJob, itemJob) => {
[8871]277              lock (downloadFinishedLocker) {
278                log.LogMessage(string.Format("Finished downloading task {0}", localJob.Id));
279                HiveTask localHiveTask = GetHiveTaskById(localJob.Id);
[6976]280
[8871]281                if (itemJob == null) {
282                  // something bad happened to this task. bad task, BAAAD task!
283                  localHiveTask.IsDownloading = false;
[6976]284                } else {
[8871]285                  // if the task is paused, download but don't integrate into parent optimizer (to avoid Prepare)
286                  if (localJob.State == TaskState.Paused) {
287                    localHiveTask.ItemTask = itemJob;
[6976]288                  } else {
[8871]289                    if (localJob.ParentTaskId.HasValue) {
290                      HiveTask parentHiveTask = GetHiveTaskById(localJob.ParentTaskId.Value);
291                      parentHiveTask.IntegrateChild(itemJob, localJob.Id);
292                    } else {
293                      localHiveTask.ItemTask = itemJob;
294                    }
[6976]295                  }
[8871]296                  localHiveTask.IsDownloading = false;
297                  localHiveTask.Task.LastTaskDataUpdate = lightweightTask.LastTaskDataUpdate;
[6976]298                }
299              }
300            });
301          }
302        }
303      }
304      GC.Collect(); // force GC, because .NET is too lazy here (deserialization takes a lot of memory)
305      if (AllJobsFinished()) {
306        this.ExecutionState = Core.ExecutionState.Stopped;
307        StopResultPolling();
308      }
309      UpdateTotalExecutionTime();
310      UpdateStatistics();
311      OnStateLogListChanged();
[7782]312      OnTaskReceived();
[6976]313    }
314
[7218]315    public HiveTask GetHiveTaskById(Guid jobId) {
316      foreach (HiveTask t in this.HiveTasks) {
317        var hj = t.GetHiveTaskByTaskId(jobId);
[6976]318        if (hj != null)
319          return hj;
320      }
321      return null;
322    }
323
324    private void UpdateStatistics() {
325      var jobs = this.GetAllHiveTasks();
326      job.JobCount = jobs.Count();
327      job.CalculatingCount = jobs.Count(j => j.Task.State == TaskState.Calculating);
328      job.FinishedCount = jobs.Count(j => j.Task.State == TaskState.Finished);
329      OnJobStatisticsChanged();
330    }
331
332    public bool AllJobsFinished() {
333      return this.GetAllHiveTasks().All(j => (j.Task.State == TaskState.Finished
334                                                   || j.Task.State == TaskState.Aborted
335                                                   || j.Task.State == TaskState.Failed)
[8871]336                                                   && !j.IsDownloading);
[6976]337    }
338
339    private void jobResultPoller_ExceptionOccured(object sender, EventArgs<Exception> e) {
[7409]340      OnExceptionOccured(sender, e.Value);
[6976]341    }
342    private void jobDownloader_ExceptionOccured(object sender, EventArgs<Exception> e) {
[7409]343      OnExceptionOccured(sender, e.Value);
[6976]344    }
345    public void UpdateTotalExecutionTime() {
346      this.ExecutionTime = TimeSpan.FromMilliseconds(this.GetAllHiveTasks().Sum(x => x.Task.ExecutionTime.TotalMilliseconds));
347    }
348    #endregion
349
350    #region Job Events
351    private void RegisterJobEvents() {
352      job.ToStringChanged += new EventHandler(OnToStringChanged);
353      job.PropertyChanged += new PropertyChangedEventHandler(job_PropertyChanged);
354      job.ItemImageChanged += new EventHandler(job_ItemImageChanged);
355      job.ModifiedChanged += new EventHandler(job_ModifiedChanged);
356    }
357
358    private void DergisterJobEvents() {
359      job.ToStringChanged -= new EventHandler(OnToStringChanged);
360      job.PropertyChanged -= new PropertyChangedEventHandler(job_PropertyChanged);
361      job.ItemImageChanged -= new EventHandler(job_ItemImageChanged);
362      job.ModifiedChanged -= new EventHandler(job_ModifiedChanged);
363    }
364    #endregion
365
366    #region Event Handler
367    public event EventHandler RefreshAutomaticallyChanged;
368    private void OnRefreshAutomaticallyChanged() {
369      var handler = RefreshAutomaticallyChanged;
370      if (handler != null) handler(this, EventArgs.Empty);
371    }
372
373    public event EventHandler JobChanged;
374    private void OnJobChanged() {
375      var handler = JobChanged;
376      if (handler != null) handler(this, EventArgs.Empty);
377    }
378
379    public event EventHandler ModifiedChanged;
380    private void job_ModifiedChanged(object sender, EventArgs e) {
381      var handler = ModifiedChanged;
382      if (handler != null) handler(sender, e);
383    }
384
385    public event EventHandler ItemImageChanged;
386    private void job_ItemImageChanged(object sender, EventArgs e) {
387      var handler = ItemImageChanged;
388      if (handler != null) handler(this, e);
389    }
390
391    public event PropertyChangedEventHandler PropertyChanged;
392    private void job_PropertyChanged(object sender, PropertyChangedEventArgs e) {
393      this.IsSharable = job.Permission == Permission.Full;
394      this.IsControllable = job.Permission == Permission.Full;
395
396      var handler = PropertyChanged;
397      if (handler != null) handler(sender, e);
398    }
399
400    public event EventHandler ToStringChanged;
401    private void OnToStringChanged(object sender, EventArgs e) {
402      var handler = ToStringChanged;
403      if (handler != null) handler(this, e);
404    }
405
406    public event EventHandler IsDownloadableChanged;
407    private void OnIsDownloadableChanged() {
408      var handler = IsDownloadableChanged;
409      if (handler != null) handler(this, EventArgs.Empty);
410    }
411
412    public event EventHandler IsControllableChanged;
413    private void OnIsControllableChanged() {
414      var handler = IsControllableChanged;
415      if (handler != null) handler(this, EventArgs.Empty);
416    }
417
418    public event EventHandler IsSharableChanged;
419    private void OnIsSharableChanged() {
420      var handler = IsSharableChanged;
421      if (handler != null) handler(this, EventArgs.Empty);
422    }
423
424    public event EventHandler IsAllowedPrivilegedChanged;
425    private void OnIsAllowedPrivilegedChanged() {
426      var handler = IsAllowedPrivilegedChanged;
427      if (handler != null) handler(this, EventArgs.Empty);
428    }
429
430    public event EventHandler JobStatisticsChanged;
431    private void OnJobStatisticsChanged() {
432      var handler = JobStatisticsChanged;
433      if (handler != null) handler(this, EventArgs.Empty);
434    }
435
436    public event EventHandler<EventArgs<Exception>> ExceptionOccured;
[7409]437    private void OnExceptionOccured(object sender, Exception exception) {
[6976]438      log.LogException(exception);
439      var handler = ExceptionOccured;
[7409]440      if (handler != null) handler(sender, new EventArgs<Exception>(exception));
[6976]441    }
442
443    public event EventHandler StateLogListChanged;
444    private void OnStateLogListChanged() {
445      var handler = StateLogListChanged;
446      if (handler != null) handler(this, EventArgs.Empty);
447    }
448
449    public event EventHandler ExecutionTimeChanged;
450    protected virtual void OnExecutionTimeChanged() {
451      var handler = ExecutionTimeChanged;
452      if (handler != null) handler(this, EventArgs.Empty);
453    }
454
455    public event EventHandler ExecutionStateChanged;
456    protected virtual void OnExecutionStateChanged() {
457      var handler = ExecutionStateChanged;
458      if (handler != null) handler(this, EventArgs.Empty);
459    }
[7782]460    public event EventHandler TaskReceived;
461    protected virtual void OnTaskReceived() {
462      var handler = TaskReceived;
463      if (handler != null) handler(this, EventArgs.Empty);
464    }
[8156]465    public event EventHandler IsProgressingChanged;
466    private void OnIsProgressingChanged() {
467      var handler = IsProgressingChanged;
468      if (handler != null) handler(this, EventArgs.Empty);
469    }
[6976]470    #endregion
471
472    #region HiveTasks Events
473    private void RegisterHiveJobsEvents() {
474      this.hiveTasks.ItemsAdded += new CollectionItemsChangedEventHandler<HiveTask>(hivetasks_ItemsAdded);
475      this.hiveTasks.ItemsRemoved += new CollectionItemsChangedEventHandler<HiveTask>(hiveTasks_ItemsRemoved);
476      this.hiveTasks.CollectionReset += new CollectionItemsChangedEventHandler<HiveTask>(hiveTasks_CollectionReset);
477    }
478
479    private void DeregisterHiveJobsEvents() {
480      this.hiveTasks.ItemsAdded -= new CollectionItemsChangedEventHandler<HiveTask>(hivetasks_ItemsAdded);
481      this.hiveTasks.ItemsRemoved -= new CollectionItemsChangedEventHandler<HiveTask>(hiveTasks_ItemsRemoved);
482      this.hiveTasks.CollectionReset -= new CollectionItemsChangedEventHandler<HiveTask>(hiveTasks_CollectionReset);
483    }
484
485    private void hiveTasks_CollectionReset(object sender, CollectionItemsChangedEventArgs<HiveTask> e) {
486      foreach (var item in e.Items) {
487        item.StateLogChanged -= new EventHandler(item_StateLogChanged);
488      }
489      OnHiveTasksReset(e);
490    }
491
492    private void hiveTasks_ItemsRemoved(object sender, CollectionItemsChangedEventArgs<HiveTask> e) {
493      foreach (var item in e.Items) {
494        item.StateLogChanged -= new EventHandler(item_StateLogChanged);
495      }
496      OnHiveTasksRemoved(e);
497    }
498
499    private void hivetasks_ItemsAdded(object sender, CollectionItemsChangedEventArgs<HiveTask> e) {
500      foreach (var item in e.Items) {
501        item.StateLogChanged += new EventHandler(item_StateLogChanged);
502        item.IsControllable = this.IsControllable;
503      }
504      OnHiveTasksAdded(e);
505    }
506
507    private void item_StateLogChanged(object sender, EventArgs e) {
508      OnStateLogListChanged();
509    }
510    #endregion
511
512    public event EventHandler HiveTasksChanged;
513    protected virtual void OnHiveTasksChanged() {
[8090]514      StopResultPolling();
[6976]515      if (this.HiveTasks != null && this.HiveTasks.Count > 0 && this.GetAllHiveTasks().All(x => x.Task.Id != Guid.Empty)) {
516        if (IsFinished()) {
517          this.ExecutionState = Core.ExecutionState.Stopped;
518          this.RefreshAutomatically = false;
[8090]519          if (jobResultPoller != null) DeregisterResultPollingEvents();
520        } else {
521          this.RefreshAutomatically = true;
[6976]522        }
523      }
524
525      var handler = HiveTasksChanged;
526      if (handler != null) handler(this, EventArgs.Empty);
527    }
528
529    public event EventHandler Loaded;
530    public virtual void OnLoaded() {
531      this.UpdateTotalExecutionTime();
[7056]532      this.OnStateLogListChanged();
[6976]533
534      if (this.ExecutionState != ExecutionState.Stopped) {
535        this.RefreshAutomatically = true;
536      }
537
538      var handler = Loaded;
539      if (handler != null) handler(this, EventArgs.Empty);
540    }
541
542    public event EventHandler<CollectionItemsChangedEventArgs<HiveTask>> HiveTasksAdded;
543    private void OnHiveTasksAdded(CollectionItemsChangedEventArgs<HiveTask> e) {
544      var handler = HiveTasksAdded;
545      if (handler != null) handler(this, e);
546    }
547
548    public event EventHandler<CollectionItemsChangedEventArgs<HiveTask>> HiveTasksRemoved;
549    private void OnHiveTasksRemoved(CollectionItemsChangedEventArgs<HiveTask> e) {
550      var handler = HiveTasksRemoved;
551      if (handler != null) handler(this, e);
552    }
553
554    public event EventHandler<CollectionItemsChangedEventArgs<HiveTask>> HiveTasksReset;
555    private void OnHiveTasksReset(CollectionItemsChangedEventArgs<HiveTask> e) {
556      var handler = HiveTasksReset;
557      if (handler != null) handler(this, e);
558    }
559
560    public Guid Id {
561      get { return job.Id; }
562      set { job.Id = value; }
563    }
564    public bool Modified {
565      get { return job.Modified; }
566    }
567    public void Store() {
568      job.Store();
569    }
570    public string ItemDescription {
571      get { return job.ItemDescription; }
572    }
573    public Image ItemImage {
574      get { return job.ItemImage; }
575    }
576    public string ItemName {
577      get { return job.ItemName; }
578    }
579    public Version ItemVersion {
580      get { return job.ItemVersion; }
581    }
582
583    public override string ToString() {
584      return string.Format("{0} {1}", Job.DateCreated.ToString("MM.dd.yyyy HH:mm"), Job.ToString());
585    }
586
587    public bool IsFinished() {
588      return HiveTasks != null
589        && HiveTasks.All(x => x.Task.DateFinished.HasValue && x.Task.DateCreated.HasValue);
590    }
591
592    public IEnumerable<HiveTask> GetAllHiveTasks() {
593      if (hiveTasks == null) return Enumerable.Empty<HiveTask>();
594
595      var tasks = new List<HiveTask>();
596      foreach (HiveTask task in HiveTasks) {
597        tasks.AddRange(task.GetAllHiveTasks());
598      }
599      return tasks;
600    }
601
602    public int CompareTo(RefreshableJob other) {
603      return this.ToString().CompareTo(other.ToString());
604    }
605  }
606}
Note: See TracBrowser for help on using the repository browser.