The Task Processor

On the server side, a separate application processes tasks one at a time. This application could be modeled as a Windows service (as a similar application was in Chapter 16); but in this case, a Windows Forms application is used, both to allow for a rich user interface complete with a displayed log of information and to demonstrate a few important threading techniques.

The task processor is actually split into two assemblies: a TaskProcessor library component that encapsulates the custom rendering code, and a TaskProcessorUI executable application. The TaskProcessorUI application scans through the list of available tasks using the database component and processes each one using the TaskProcessor component.

The TaskProcessor Component

Listing 18-25 shows the complete TaskProcessor component. In this case, a WasteTime method is used to stall processing for three minutes, loosely simulating the rendering process.

Listing 18-25 The TaskProcessor component
 Public Class TaskProcessor     Public Sub RenderFile(ByVal sourceFile As String, _       ByVal targetFile As String)         ' This is where the business-specific code for rendering the         ' files would go. In this example, the code simply stalls for         ' three minutes while "processing" the file.         WasteTime(180)         ' Create the rendered (in this case, empty) file.         Dim fs As New FileStream(targetFile, FileMode.CreateNew)         fs.Close()     End Sub     Private Sub WasteTime(ByVal seconds As Integer)         Dim StartTime As DateTime = DateTime.Now         Do         Loop Until DateTime.Now > (StartTime.AddSeconds(seconds))     End Sub End Class 

The Task Processor Front End

The task processor application provides a simple user interface that enables the user to quickly suspend and resume processing (as shown in Figure 18-8). This allows the server to be freed up for more time-sensitive tasks as required.

Figure 18-8. The task processor interface

graphics/f18dp08.jpg

A dedicated object on a separate thread performs all the processing work. This allows the front end to remain responsive, even while the rendering operation is underway. Listing 18-26 shows the code used to set up these objects when the program starts.

Listing 18-26 The task processor initialization code
 Private WorkerObject As ProcessThread Private WorkerThread As Thread Private Sub Main_Load(ByVal sender As System.Object, _   ByVal e As System.EventArgs) Handles MyBase.Load     ' Set up the worker object that will perform the work.     WorkerObject = New ProcessThread(Me)     ' Set up the thread where the work will be performed.     WorkerThread = New Thread(AddressOf WorkerObject.DoWork)     ' Start the worker process.     WorkerThread.Start()     Display("Worker thread started.") End Sub Public Sub Display(ByVal message As String)     txtStatus.Text &= message     txtStatus.Text &= System.Environment.NewLine End Sub 

Is This the Best Use of Multiple CPUs?

As described previously, the TaskProcessorUI application renders only one file at a time. However, the server on which this application runs has multiple CPUs available. In this case, we assume that the actual task-processing code has been optimized to use multiple CPUs. This ensures that each individual task will complete several times faster than it would otherwise. If the task-processing code were not optimized, however, you could obtain better performance by creating a more complicated multithreaded front-end application that creates several worker threads that run at the same time.

To make this transition easier, we've designed the front-end application to perform all its work using a custom threaded object. If you decide to implement multiple worker threads in your front-end application, refer to the singleton design shown in Chapter 7. You can control the number of active threads using a preset maximum, or you can make use of the .NET ThreadPool object for automatic thread pooling.

Remember that creating a multithreaded front end can increase the overall throughput of the application (because it allows several tasks to be processed at the same time), but it won't reduce the time taken to process an individual task. On the other hand, a multithreaded task processor can both increase the overall system throughput and reduce the time taken for a single task, which often makes it a better choice.

Listing 18-27 shows the threaded worker object without the actual processing logic (which is contained in the DoWork method).

Listing 18-27 The threaded worker object
 Public Class ProcessThread     Private SourcePath As String     Private TargetPath As String     Private ServerPrefix As String     Private HostForm As Main     Private Tables As New DBComponent.SuperComputeTables()     Private Processor As New TaskProcessor() 
     Public RequestStop As Boolean = False     Public Sub New(ByVal form As Main)         SourcePath = ConfigurationSettings.AppSettings( _                       "SourceDirectory")         TargetPath = ConfigurationSettings.AppSettings( _                       "CompleteDirectory")         ServerPrefix = ConfigurationSettings.AppSettings( _                       "ServerPrefix")         HostForm = form     End Sub     Public Sub DoWork()         ' (Code omitted)     End Sub End Class 

The worker object reads the directories it should process from the configuration file shown in Listing 18-28. In addition, it stores a Server prefix that is used to create the UNC path for the rendered filename, which will be stored in the database and used by the client to retrieve the final file.

Listing 18-28 The task processor configuration file
 <?xml version="1.0" encoding="utf-8" ?> <configuration>   <appSettings>     <add key="SuperComputeConnection"          value="Data Source=localhost;Initial                  Catalog=SuperCompute;Integrated Security=SSPI" />     <add key="SourceDirectory"          value="c:\SuperCompute\Source" />     <add key="CompleteDirectory"          value="c:\SuperCompute\Complete" />     <add key="ServerPrefix"          value="\\localhost\CaseStudies\SuperCompute\Complete" />   </appSettings> </configuration> 

The DoWork subroutine shown in Listing 18-29 retrieves all the available tasks from the database, determines the appropriate source and target filenames, and then submits a request to the TaskProcessor component. If there are no waiting requests, the thread is put to sleep for 5 minutes before it requeries the database.

Listing 18-29 Processing tasks
 Public Sub DoWork()     Dim Task As DBComponent.TaskDetails     Dim Tasks As DBComponent.TaskDetails()     Do         ' Retrieve all tasks that need to be processed.         Tasks = Tables.Tasks.GetAvailableTasks()         For Each Task In Tasks             ' Mark the record in progress.             Tables.Tasks.UpdateTaskStatus(Task.TaskGUID, _               DBComponent.TaskStatus.InProgress)             Dim Source, Target As String             FileToRender = SourcePath & "\" & Task.TaskGUID.ToString()             Target = TargetPath & "\" & Task.TaskGUID.ToString()             ' Perform the rendering.             Processor.RenderFile(Source, Target)             Task.RenderedFileUrl = ServerPrefix & "/" & _                                    Task.TaskGUID.ToString()             Task.Status = DBComponent.TaskStatus.Complete             ' Update the task record.             Tables.Tasks.CompleteTask(Task.TaskGUID, _               Task.RenderedFileUrl)             ' Check if the user is trying to close the application.             If RequestStop Then Return         Next         ' There are no more files.         ' Pause for five minutes before the next check.         Thread.Sleep(TimeSpan.FromMinutes(5))     Loop End Sub 

Listing 18-29 leaves out two details. First, the DoWork method actually performs the rendering in an exception handler. If an exception occurs, it then updates the task status to indicate that an error occurred and to ensure that it does not attempt to render the file again (which could be a considerable waste of time if a source file error causes a recurring error).

One type of exception gets special treatment, however: the ThreadAbort­Exception. The ThreadAbortException only occurs if a user attempts to shut down the application while it is in the middle of an operation and the thread can't complete the current operation and end gracefully in the allowed time. A ThreadAbortException does not indicate a problem in the file being rendered. Listing 18-30 shows the error-handling code used to deal with this occurrence.

Listing 18-30 Exception handling in DoWork
 Public Sub DoWork()     Dim Task As DBComponent.TaskDetails     Dim Tasks As DBComponent.TaskDetails()     Do         Tasks = Tables.Tasks.GetAvailableTasks()         For Each Task In Tasks             Try                 ' (Processing code omitted.)             Catch Err As ThreadAbortException                 ' This error only occurs if the thread cannot                 ' end fast enough when a stop is requested. There                 ' is no database problem, so no action is performed.             Catch                 ' This is a miscellaneous update error.                 ' When an error is encountered, the status of the file                 ' is updated.                 ' This is also a good place to add custom logging code,                 ' or even write a more descriptive error code to the                 ' task record in the database.                 Tables.Tasks.UpdateTaskStatus(Task.TaskGUID, _                   DBComponent.TaskStatus.HaltedWithError)             End Try             If RequestStop Then Return         Next         Thread.Sleep(TimeSpan.FromMinutes(5))     Loop End Sub 

The worker thread also provides messages that need to appear directly in the user interface. You can accommodate this design in a number of ways, but the most direct way is just to marshal code to the user-interface thread. The ProcessThread class provides a HostForm property just for this purpose; it stores a reference to the user-interface form.

Listing 18-31 shows the logging code for the DoWork method and the helper methods that allow the call to be marshaled to the user-interface thread.

Listing 18-31 User interface messages in DoWork
 Public Sub DoWork()     Dim Task As DBComponent.TaskDetails     Dim Tasks As DBComponent.TaskDetails()     Do         Tasks = Tables.Tasks.GetAvailableTasks()         For Each Task In Tasks             Try                 Display("Starting: " + Task.TaskGUID.ToString())                 ' (Processing code omitted.)                 Display("Rendered: " + Task.TaskGUID.ToString())             Catch Err As ThreadAbortException             Catch                 Tables.Tasks.UpdateTaskStatus(Task.TaskGUID, _                 DBComponent.TaskStatus.HaltedWithError)                 Display("Error with: " + Task.TaskGUID.ToString())             End Try             If RequestStop Then Return         Next         Thread.Sleep(TimeSpan.FromMinutes(5))     Loop End Sub 
 ' This method allows a method call to be marshaled to  ' the user interface thread, allowing a label control update. Private Sub Display(ByVal message As String)     NewText = message     HostForm.Invoke(New MethodInvoker(AddressOf Me.AddText)) End Sub Private NewText As String ' This method executes on the form thread. Private Sub AddText()     HostForm.Display(NewText)   End Sub 

Finally, the application needs to take extra care when shutting down. It attempts to terminate the thread gracefully by raising a flag, as shown in Listing 18-32. Note that if the thread is currently suspended, it must be resumed before it can be aborted. If the code fails to end the thread after 3 minutes, the thread is aborted manually. Alternatively, you can set the timeout limit based on a configuration file setting.

Listing 18-32 Closing the task processor
 Private Sub Main_Closing(ByVal sender As Object, _   ByVal e As System.ComponentModel.CancelEventArgs) _   Handles MyBase.Closing      ' Set the stop flag so the thread can finish the current task.      WorkerObject.RequestStop = True     If (WorkerThread.ThreadState And ThreadState.WaitSleepJoin) = _       ThreadState.WaitSleepJoin Then         ' Thread has put itself to sleep.         ' It is safe to end the thread.         WorkerThread.Interrupt()         WorkerThread.Abort()     Else         If (WorkerThread.ThreadState And ThreadState.Suspended) = _           ThreadState.Suspended Then             WorkerThread.Resume()         End If         ' Attempt to let the worker thread end gracefully.         WorkerThread.Join(TimeSpan.FromMinutes(3)) 
         If (WorkerThread.ThreadState And ThreadState.Running) = _           ThreadState.Running Then             ' Thread is still running.             ' Time to end it the hard way.             WorkerThread.Abort()         End If     End If End Sub 

Note

When you test more than one component of this project at the same time, you should use separate instances of Visual Studio .NET or run some of the applications outside the development environment. If you configure your project to start multiple applications for debugging, you might experience a significant slowdown when you attempt to use the client application while a task is being processed. Instead, you should run these applications outside the IDE when you want to test the full solution at once.


Downloading a Rendered File

The final ingredient is allowing the client to download the rendered file. A MouseDown event handler is added to the DataGrid for this purpose. (See Listing 18-33.) When a right-click is detected on a row that corresponds to a completed item, a special shortcut menu is displayed (as shown in Figure 18-9).

Figure 18-9. Retrieving the rendered file in the client

graphics/f18dp09.jpg

Listing 18-33 Showing the right-click download option
 Private Sub gridTasks_MouseDown(ByVal sender As Object, _   ByVal e As System.Windows.Forms.MouseEventArgs) _   Handles gridTasks.MouseDown     If e.Button = MouseButtons.Right Then         Dim HitInfo As DataGrid.HitTestInfo         HitInfo = gridTasks.HitTest(e.X, e.Y)         If HitInfo.Type = DataGrid.HitTestType.Cell Then             If gridTasks.Item(HitInfo.Row, colRendered).ToString() _              <> "" Then                 ' This file can be downloaded. Select the row and                 ' show menu.                 gridTasks.CurrentRowIndex = HitInfo.Row                 mnuGrid.Show(gridTasks, New Point(e.X, e.Y))             End If         End If     End If End Sub 

If the user chooses the download menu item, a common Save dialog box appears that enables the user to specify where the file should be placed. (See Listing 18-34.) As a nicety, the filename defaults to the original client source name plus the extension rendered.

Listing 18-34 Transferring the rendered file
 Private Sub mnuDownload_Click(ByVal sender As System.Object, _   ByVal e As System.EventArgs) Handles mnuDownload.Click     ' Determine the UNC path to the rendered file.     Dim FileToDownload As String     FileToDownload = gridTasks.Item( _                      gridTasks.CurrentRowIndex, colRendered).ToString()     ' Default the target filename based on the source filename.     dlgSave.FileName = gridTasks.Item(gridTasks.CurrentRowIndex, _                        colSource).ToString() & ".rendered"     If dlgSave.ShowDialog() = DialogResult.OK Then         ' Copy the file.         File.Copy(FileToDownload, dlgSave.FileName)     End If End Sub 

Note that there is currently no way to remove task records from the database. These records should not be deleted automatically when a file is downloaded because they might be useful in the future for tracking old files or reviewing usage information. The best approach is probably to add a new status code that indicates a task that has completed and has been downloaded. The GetTasksByUser stored procedure can then be modified to ignore records that have been downloaded.



Microsoft. NET Distributed Applications(c) Integrating XML Web Services and. NET Remoting
MicrosoftВ® .NET Distributed Applications: Integrating XML Web Services and .NET Remoting (Pro-Developer)
ISBN: 0735619336
EAN: 2147483647
Year: 2005
Pages: 174

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net