简体   繁体   English

通过基于任务的异步模式 (TAP) 接触的 Windows 服务作业监视器

[英]Windows Service Job Monitor via Touch of Task-based Asynchronous Pattern (TAP)

I am building a windows service in C# that will monitor a job queue and when it finds item(s) available in the queue, it launches jobs that will 'completely' process the job (including failures).我正在用 C# 构建一个 Windows 服务,它将监视一个作业队列,当它在队列中找到可用的项目时,它会启动将“完全”处理该作业(包括失败)的作业。 I'm using Task.Factory.StartNew() and admittedly, I'm very green in TAP (heading off to read blogs after finishing this post).我正在使用 Task.Factory.StartNew() 并且无可否认,我对 TAP 非常熟悉(在完成这篇文章后开始阅读博客)。

Requirements要求

  1. Poll a database queue at regular intervals looking for available jobs.定期轮询数据库队列以查找可用作业。 (let's ignore the argument of messaging queue vs database queue for the purpose of this question) (为了这个问题,让我们忽略消息队列与数据库队列的论点)

  2. Launch jobs asynchronously so that the polling can continue to monitor the queue and launch new jobs.异步启动作业,以便轮询可以继续监视队列并启动新作业。

  3. Honor a 'job' threshold so that too many jobs do not get spawned.遵守“工作”门槛,以免产生过多工作。

  4. 'Delay' the shutdown of the service if jobs are processing.如果作业正在处理,则“延迟”关闭服务。

  5. Make sure failures in jobs are logged to the Event Log and do not crash the Windows Service.确保将作业中的失败记录到事件日志中,并且不会使 Windows 服务崩溃。

My code is below, but here are my questions/concerns (and if there is better place to post this, let me know) but this mostly revolves around proper use of TAP and 'stability' of it.我的代码在下面,但这里是我的问题/疑虑(如果有更好的地方可以发布此内容,请告诉我)但这主要围绕 TAP 的正确使用和它的“稳定性”。 Note that most of my questions are documented in code as well.请注意,我的大部分问题也记录在代码中。

Questions问题

  1. In PollJobs, is the way I use Task.Factory.Start/ContinueWith the appropriate usage to keep the throughput high with this job processing service?在 PollJobs 中,我使用 Task.Factory.Start/ContinueWith 的方式是否正确使用此作业处理服务来保持高吞吐量? I'll never be blocking any threads and hopefully using the correct pattern for the tiny bit of TAP I currently have.我永远不会阻塞任何线程,并希望对我目前拥有的一点点 TAP 使用正确的模式。

  2. ConcurrentDictionary - Using this to monitor currently running jobs, and each jobs as it finishes, removes itself from the dictionary (on separate threads I assume from the Task.Factory.StartNew), because it is ConcurrentDictionary, I assume I don't need any locks anywhere when using it? ConcurrentDictionary - 使用它来监视当前正在运行的作业,并且每个作业在完成时都会从字典中删除自己(在我从 Task.Factory.StartNew 假设的单独线程上),因为它是 ConcurrentDictionary,我假设我不需要任何使用时随处锁定?

  3. Job Exceptions (worst one being OutOfMemoryException) - Any exceptions during job processing cannot bring down the service and must be logged correctly in Event Log and database queue.作业异常(最糟糕的是 OutOfMemoryException) - 作业处理期间的任何异常都无法关闭服务,必须正确记录在事件日志和数据库队列中。 Currently there are jobs that unfortunately can throw OutOfMemoryException.目前,不幸的是,有些作业会抛出 OutOfMemoryException。 Is the try/catch inside the 'job processing' good enough to catch and handle all scenarios so that the Windows Service will never terminate unexpectedly? “作业处理”中的 try/catch 是否足以捕获和处理所有场景,以便 Windows 服务永远不会意外终止? Or would it be better/safer to launch an AppDomain for each job for more isolation?或者为每个工作启动一个 AppDomain 以获得更多隔离会更好/更安全吗? (over kill?) (过杀?)

  4. I've seen arguments debating the 'proper' Timer to use with no decent answers.我见过争论使用“正确”计时器的争论,但没有像样的答案。 Any opinion on my setup and use of my System.Threading.Timer?对我的 System.Threading.Timer 的设置和使用有什么意见吗? (specifically around how I ensure PollJobs is never called again until the previous call finishes) (特别是关于我如何确保在上一次调用完成之前永远不会再次调用 PollJobs)

If you've made it this far.如果你已经做到了这一步。 Thanks in advance.提前致谢。

Code代码

public partial class EvolutionService : ServiceBase
{
    EventLog eventLog = new EventLog() { 
        Source = "BTREvolution", 
        Log = "Application" 
    };
    Timer timer;
    int pollingSeconds = 1;

    // Dictionary of currently running jobs so I can query current workload.  
    // Since ConcurrentDictionary, hoping I don't need any lock() { } code.
    private ConcurrentDictionary<Guid, RunningJob> runningJobs = 
        new ConcurrentDictionary<Guid, RunningJob>();

    public EvolutionService( string[] args )
    {
        InitializeComponent();

        if ( !EventLog.SourceExists( eventLog.Source ) )
        {
            EventLog.CreateEventSource( 
                eventLog.Source, 
                eventLog.Log );
        }
    }

    protected override void OnStart( string[] args )
    {
        // Only run polling code one time and the PollJobs will 
        // initiate next poll interval so that PollJobs is never 
        // called again before it finishes its processing, http://stackoverflow.com/a/1698409/166231
        timer = new System.Threading.Timer( 
            PollJobs, null, 
            TimeSpan.FromSeconds( 5 ).Milliseconds, 
            Timeout.Infinite );
    }

    protected override void OnPause()
    {
        // Disable the timer polling so no more jobs are processed
        timer = null;

        // Don't allow pause if any jobs are running
        if ( runningJobs.Count > 0 )
        {
            var searcher = new System.Management.ManagementObjectSearcher( 
                "SELECT UserName FROM Win32_ComputerSystem" );
            var collection = searcher.Get();
            var username = 
                (string)collection
                    .Cast<System.Management.ManagementBaseObject>()
                    .First()[ "UserName" ];
            throw new InvalidOperationException( $"{username} requested pause.  The service will not process incoming jobs, but it must finish the {runningJobs.Count} job(s) are running before it can be paused." );
        }

        base.OnPause();
    }

    protected override void OnContinue()
    {
        // Tell time to start polling one time in 5 seconds
        timer = new System.Threading.Timer( 
            PollJobs, null, 
            TimeSpan.FromSeconds( 5 ).Milliseconds, 
            Timeout.Infinite );
        base.OnContinue();
    }

    protected override void OnStop()
    {
        // Disable the timer polling so no more jobs are processed
        timer = null;

        // Until all jobs successfully cancel, keep requesting more time
        // http://stackoverflow.com/a/13952149/166231
        var task = Task.Run( () =>
        {
            // If any running jobs, send the Cancel notification
            if ( runningJobs.Count > 0 )
            {
                foreach ( var job in runningJobs )
                {
                    eventLog.WriteEntry( 
                        $"Cancelling job {job.Value.Key}" );
                    job.Value.CancellationSource.Cancel();
                }
            }

            // When a job cancels (and thus completes) it'll 
            // be removed from the runningJobs workload monitor.  
            // While any jobs are running, just wait another second
            while ( runningJobs.Count > 0 )
            {
                Task.Delay( TimeSpan.FromSeconds( 1 ) ).Wait();
            }
        } );

        // While the task is not finished, every 5 seconds 
        // I'll request an additional 5 seconds
        while ( !task.Wait( TimeSpan.FromSeconds( 5 ) ) )
        {
            RequestAdditionalTime( 
                TimeSpan.FromSeconds( 5 ).Milliseconds );
        }
    }

    public void PollJobs( object state )
    {
        // If no jobs processed, then poll at regular interval
        var timerDue = 
            TimeSpan.FromSeconds( pollingSeconds ).Milliseconds;

        try
        {
            // Could define some sort of threshhold here so it 
            // doesn't get too bogged down, might have to check
            // Jobs by type to know whether 'batch' or 'single' 
            // type jobs, for now, just not allowing more than
            // 10 jobs to run at once.
            var availableProcesses = 
                Math.Max( 0, 10 - runningJobs.Count );

            if ( availableProcesses == 0 ) return;

            var availableJobs = 
                JobProvider.TakeNextAvailableJobs( availableProcesses );
            foreach ( var j in availableJobs )
            {
                // If any jobs processed, poll immediately when finished
                timerDue = 0;

                var job = new RunningJob
                {
                    Key = j.jKey,
                    InputPackage = j.JobData.jdInputPackage,
                    DateStart = j.jDateStart.Value,
                    CancellationSource = new CancellationTokenSource()
                };

                // Add running job to the workload monitor
                runningJobs.AddOrUpdate(
                    j.jKey,
                    job,
                    ( key, info ) => null );

                Task.Factory
                    .StartNew(
                        i =>
                        {
                            var t = (Tuple<Guid, CancellationToken>)i;

                            var key = t.Item1; // Job Key
                            // Running process would check if cancel has been requested
                            var token = t.Item2; 
                            var totalProfilesProcess = 1;

                            try
                            {
                                eventLog.WriteEntry( $"Running job {key}" );

                                // All code in here completes the jobs.
                                // Will be a seperate method per JobType.
                                // Any exceptions in here *MUST NOT* 
                                // take down service.  Before allowing 
                                // the exception to propogate back up 
                                // into *this* try/catch, the code must 
                                // successfully clean up any resources 
                                // and state that was being modified so 
                                // that the client who submitted this 
                                // job is properly notified.

                                // This is just simulation of running a 
                                // job...so each job takes 10 seconds.
                                for ( var d = 0; d < 10; d++ )
                                {
                                    // or could do await if I called Unwrap(),
                                    // https://blogs.msdn.microsoft.com/pfxteam/2011/10/24/task-run-vs-task-factory-startnew/
                                    Task.Delay( 1000 ).Wait();
                                    totalProfilesProcess++;

                                    if ( token.IsCancellationRequested )
                                    {
                                        // TODO: Clean up the job
                                        throw new OperationCanceledException( token );
                                    }
                                }

                                // Success
                                JobProvider.UpdateJobStatus( key, 2, totalProfilesProcess );
                            }
                            catch ( OperationCanceledException )
                            {
                                // Cancelled
                                JobProvider.UpdateJobStatus( key, 3, totalProfilesProcess );
                                throw;
                            }
                            catch ( Exception )
                            {
                                // Failed
                                JobProvider.UpdateJobStatus( key, 4, totalProfilesProcess );
                                throw;
                            }
                        },
                        // Pass cancellation token to job so it can watch cancel request
                        Tuple.Create( j.jKey, job.CancellationSource.Token ),
                        // associate cancellation token with Task started via StartNew()
                        job.CancellationSource.Token, 
                        TaskCreationOptions.LongRunning,
                        TaskScheduler.Default

                    ).ContinueWith(
                        ( t, k ) =>
                        {
                            // When Job is finished, log exception if present.
                            // Haven't tested this yet, but think 
                            // Exception will always be AggregateException
                            // so I'll have to examine the InnerException.
                            if ( !t.IsCanceled && t.IsFaulted )
                            {
                                eventLog.WriteEntry( $"Exception for {k}: {t.Exception.Message}", EventLogEntryType.Error );
                            }

                            eventLog.WriteEntry( $"Completed job {k}" );

                            // Remove running job from the workload monitor
                            RunningJob completedJob;
                            runningJobs.TryRemove( 
                                (Guid)k, out completedJob );
                        },
                        j.jKey
                    );
            }
        }
        catch ( Exception ex )
        {
            // If can't even launch job, disable the polling.
            // TODO: Could have an error threshhold where I don't
            // shut down until X errors happens
            eventLog.WriteEntry( 
                ex.Message + "\r\n\r\n" + ex.StackTrace, 
                EventLogEntryType.Error );
            timer = null;
        }
        finally
        {
            // If timer wasn't 'terminated' in OnPause or OnStop, 
            // then set to call timer again
            if ( timer != null )
            {
                timer.Change( timerDue, Timeout.Infinite );
            }
        }
    }
}

class RunningJob
{
    public Guid Key { get; set; }
    public DateTime DateStart { get; set; }
    public XElement InputPackage { get; set; }
    public CancellationTokenSource CancellationSource { get; set; }
}

我用 Hangfire.io 解决了我的问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 从异步编程模型(APM)迁移到基于任务的异步模式(TAP) - Moving from Asynchronous Programming Model (APM) to Task-based Asynchronous Pattern (TAP) Telerik中基于任务的异步模式支持 - Task-based Asynchronous Pattern support in Telerik 具有基于任务的异步模式的UDP侦听器 - UDP listener with task-based asynchronous pattern 在.NET 4.0和C#4.0中使用基于任务的异步模式(TAP) - Use Task-based Asynchronous Pattern (TAP) with .NET 4.0 and C# 4.0 在双工WCF服务的客户端中使用基于任务的异步模式时出错 - Error when I use the Task-based Asynchronous Pattern in the client of duplex WCF service C#中基于任务的异步方法的超时模式 - Timeout pattern on task-based asynchronous method in C# Oracle Client与基于任务的异步模式(异步/等待) - Oracle Client vs. Task-based Asynchronous Pattern (async/await) 从WCF服务调用基于任务的异步单向回调方法 - Calling a task-based asynchronous one way callback method from WCF service 这是实现基于任务的异步方法的正确方法吗? - Is this correct way to implement task-based asynchronous method? 如何在没有基于任务的代理的情况下处理Windows Phone 8中的服务引用 - How to handle Service References in Windows Phone 8 without Task-Based proxies
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM