【问题标题】:Long running file operations and execution timeout长时间运行的文件操作和执行超时
【发布时间】:2013-03-26 17:22:07
【问题描述】:

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

PDF 格式的文档几乎无一例外,而且在任何时候,任何一条记录的处理时间通常少于 20 个。

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

该系统一直运行良好,直到大量文档(114 个 PDF 文档,总大小为 329MB)在 95% 左右超时。

代码是(IncomingDocuments 是 List 类型)-

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 秒 - 这可能意味着在这种情况下超时停止发生,但是如果用户正在尝试添加或发布数以千计的文档。此解决方案无法很好地扩展,只是推迟而不是解决问题。

我正在将 .NET 4 与 Visual Studio 2010 一起使用,据我所知,如果我们想打包文档并使用更新进度,则必须使用第三方的异步实现和等待,如 AsyncBridge (https://nuget.org/packages/AsyncBridge)阿贾克斯。我无法访问 Visual Studio 2012 甚至比 XP 更新的 Windows,无法使用 Microsoft 提供的 Async 目标包。

鉴于这些限制,有没有办法打包/批量处理这些文档以避免超时并(理想情况下)在添加每个批次时向用户提供反馈?如果 F# 易于实现,我愿意探索它。或者,我应该为 Visual Studio 2012 辩护吗?

【问题讨论】:

  • 我对某事感到好奇..您的组织不能使用或实施SharePoint..吗?听起来你被要求重新发明Wheel
  • 请那些反对这个问题的人解释为什么?我非常愿意以某种理由接受它。
  • 由于我们必须与第三方产品集成,Sharepoint 在这种情况下不是一个选项。

标签: c# asp.net asynchronous webforms timeout


【解决方案1】:

无需迁移到完全不同的语言或升级 IDE 工具,这不是当前的问题。目前的问题是,一个从根本上为快速响应而构建的系统(Web 应用程序)正被用于长期运行的流程。

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

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

在这种情况下,我所说的“异步”是指您希望有一个单独的服务器端进程来处理 CPU 密集型任务,而 Web 应用程序只是将任务排队等待运行并检查人们寻找它时的任务。然后它可以在完成后报告任务的结果。

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

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

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

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

【讨论】:

    猜你喜欢
    • 2019-06-16
    • 2022-06-17
    • 1970-01-01
    • 2011-04-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多