长时间运行的文件操作和执行超时

时间:2013-03-26 17:22:07

标签: c# asp.net asynchronous webforms timeout

我被要求为我工作的组织编写文档管理系统,该系统提供了一系列与不同记录相关的九个不同工作流程。其中的工作流程是将文档添加到“文件”或记录中,并根据业务规则将这些文档的子集发布到公共网站。

这些文件几乎无一例外地以PDF格式存在,并且通常在任何一条记录中,在任何时候都只有不到20个文件被处理。

将此作为Web应用程序构建的主要原因是将文件保留在我们的数据中心和高速交换机上,而不是尝试通过远程站点的可能较慢的连接速度在位置之间复制和备份。

该系统运行良好,直到更大系列的文件(114个PDF文件,总长度为329MB)超过95%的时间。

代码是(IncomingDocuments类型为List< FileInfo>) -

List<string> filesSuccessfullyAdded = new List<string>();

foreach (FileInfo incomingFile in IncomingDocuments)
{
    FileOperations.AddDocument(incomingFile, false, ApplicationCode, (targetDirectoryPath.EndsWith(@"\") ? targetDirectoryPath : targetDirectoryPath + @"\"));
    FileInfo copiedDocument = new FileInfo(Path.Combine(targetDirectoryPath, incomingFile.Name));
    if (copiedDocument.Exists && copiedDocument.Length == incomingFile.Length && copiedDocument.LastWriteTime == incomingFile.LastWriteTime)
    {
        filesSuccessfullyAdded.Add(copiedDocument.Name);
    }
}

if (filesSuccessfullyAdded.Any())
{
    SetupConfirmationLiteral.Text += "<p class='info'>The following files have been successfully added to the application file-</p>";

    XDocument successfullyAddedList = new XDocument(
    new XElement("ul", new XAttribute("class", "documentList")));

    foreach (string successfulFile in filesSuccessfullyAdded)
    {
        successfullyAddedList.Root.Add(new XElement("li", successfulFile));
    }

    SetupConfirmationLiteral.Text += successfullyAddedList.ToString();
}

var notSuccessfullyAdded = from FileInfo incomingDocument in IncomingDocuments
                            where !filesSuccessfullyAdded.Contains(incomingDocument.Name)
                            orderby incomingDocument.Name ascending
                            select incomingDocument.Name;

if (notSuccessfullyAdded.Any())
{
    SetupConfirmationLiteral.Text += "<p class='alert'>The following files have <strong>not</strong> been successfully added to the application file-</p>";

    XDocument notAddedList = new XDocument(
    new XElement("ul", new XAttribute("class", "documentList")));

    foreach (string notAdded in notSuccessfullyAdded)
    {
        notAddedList.Root.Add(new XElement("li", notAdded));
    }

    SetupConfirmationLiteral.Text += notAddedList.ToString();

    SetupConfirmationLiteral.Text += "<p>A file of the same name may already exist in the target location.</p>";
}

使用实用方法 -

public static void AddDocument(FileInfo sourceFile, bool appendName, string applicationCode, string targetPath)
{
    try
    {
        DirectoryInfo targetDirectory = new DirectoryInfo(targetPath);
        if (targetDirectory.Exists)
        {
            string targetFileName = (appendName ? sourceFile.Name.Insert(sourceFile.Name.IndexOf(sourceFile.Extension, StringComparison.Ordinal), " UPDATED") : sourceFile.Name);
            if (targetDirectory.GetFiles(targetFileName).Any())
            {
                //Do not throw an exception if the file already exists. Silently return. If the file exists and matches both last modified and size it won't be reported, and can be archived as normal,
                //otherwise it should be reported to user in the calling method.
                return;
            }
            string targetFileUnc = Path.Combine(targetPath, targetFileName);
            sourceFile.CopyTo(targetFileUnc, overwrite: false);
            Logging.FileLogEntry(username: (HttpContext.Current.User.Identity.IsAuthenticated ? HttpContext.Current.User.Identity.Name : "Unknown User"), eventType: LogEventType.AddedDocument,
                applicationCode: applicationCode, document: sourceFile.Name, uncPath: targetFileUnc);
        }
        else
        {
            throw new PdmsException("Target directory does not exist");
        }
    }
    catch (UnauthorizedAccessException ex)
    {
        throw new PdmsException("Access was denied to the target directory. Contact the Service Desk.", ex);
    }
    catch (PathTooLongException)
    {
        throw new PdmsException(string.Format("Cannot add document {0} to the Site File directory for Application {1} - the combined path is too long. Use the Add Documents workflow to re-add documents to this Site File after renaming {0} to a shorter name.", sourceFile.Name, applicationCode ));
    }
    catch (FileNotFoundException ex)
    {
        throw new PdmsException("The incoming file was not found. It may have already been added to the application file.", ex);
    }
    catch (DirectoryNotFoundException ex)
    {
        throw new PdmsException("The source or the target directory were not found. The document(s) may have already been added to the application file.", ex);
    }
    catch (IOException ex)
    {
        throw new PdmsException("Error adding files - file(s) may be locked or there may be server or network problem preventing the copy. Contact the Service Desk.", ex);
    }
}

进行实际复制和审核。 PdmsException只是一个特定的异常类,我们用它来向用户显示有用的错误消息,允许他们在可能的情况下解决他们自己的问题,或者至少给出一个可理解的失败原因。

我知道我可以简单地将ExecutionTimeout属性(http://msdn.microsoft.com/en-us/library/system.web.configuration.httpruntimesection.executiontimeout.aspx)增加到超过默认值110秒 - 最多300秒 - 这可能意味着在这种情况下超时停止发生,但是如果用户怎么办?正在尝试添加或发布数千个文档。这种解决方案不能很好地扩展,只是推迟而不是解决问题。

我在Visual Studio 2010中使用.NET 4,据我所知,如果我们想要包装文档并更新,我必须使用async的第三方实现并等待AsyncBridge(https://nuget.org/packages/AsyncBridge)使用ajax的进展。我无法访问Visual Studio 2012甚至是比XP更新的Windows,以便使用Microsoft提供的异步定位包。

鉴于这些限制,有没有办法对这些文件进行分页/批处理以避免超时,并且(理想情况下)在添加每个批次时向用户提供反馈?如果能够直接实施,我愿意去探索F#。或者我应该在这个问题上为Visual Studio 2012辩护吗?

1 个答案:

答案 0 :(得分:9)

没有必要使用完全不同的语言或升级IDE工具,这不是手头的问题。手头的问题是,一个基本上用于快速响应(Web应用程序)的系统正在用于长期运行的过程。

在Web应用程序中,任何需要花费一些时间的事情都应该异步完成。在HTTP的请求/响应模型中,最好(出于多种原因)快速响应发出请求的客户端。

对于长时间运行的过程,通过“异步”我并不是指使用AJAX,因为它仍然是一个像其他任何请求/响应一样。

通过“异步”我的意思是在这种情况下,你想要一个单独的服务器端进程来处理CPU密集型任务,而Web应用程序除了将任务排队等待运行并检查状态之外人们寻找它的任务。然后它可以在任务完成后报告任务结果。

所以架构的基本概述是这样的:

  • Web应用程序中的用户单击按钮以“启动任务”。
  • Web应用程序将记录插入到数据库表中,表明该任务已排队(可能是用户ID排队的用户ID,时间戳,您需要知道的任何其他内容)。
  • 一个单独的计划应用程序(控制台应用程序或Windows服务,很可能)是永久运行的。 (在一个正在运行的Windows服务中使用计时器或计划一次又一次地运行,例如每隔几分钟运行一次作为控制台应用程序。)此应用程序检查数据库表以查找新的排队任务。
  • 当应用程序看到任务时,它会在数据库中将其标记为“已启动”(因此应用程序的后续运行不会尝试并行运行相同的任务)并开始运行它。
  • Web应用程序可以在数据库表中查看任务的状态,并将其显示给请求它的用户,这样用户就可以看到它仍然在运行。
  • 任务完成后,数据库表中的任务记录将更新,结果将存储在某处。 (取决于结果是什么。数据?在数据库中。某种报告文件?在某处保存为文件。这完全取决于你。)
  • Web应用程序可以将任务状态视为已完成,并记录任何其他信息,用户可以请求查看任务的输出。

这里要记住的主要事情是将责任分解为两个应用程序。 Web应用程序用于提供用户界面。 Web应用程序不适合长时间运行的后台任务。因此,责任转移到更适合该目的的单独应用程序。这两个应用程序通过共享数据库进行协调。

因此,正如您在问题的最后所暗示的那样,您可以(并且应该)简单地将任务与应用程序“排队”,并以用户认为合适的各种方式管理该队列。