Посты
Обход ограничения максимального количества обрабатываемых отчетов в Crystal Reports 13
18 декабря 2013 г. • 6 мин чтения

В версии Crystal Reports 13 для Visual Studio 2012 существует пороговое значение, которое ограничивает количество одновременных отчетов, включая подотчеты, до 75 отчетов на машину. Это означает, что если на сервере работает 5 веб-приложений, все открытые отчеты во всех 5 веб-приложениях учитываются в лимите 75 отчетов.
Ошибка проявляется по-разному и может привести к следующим ошибкам: “Not enough memory for operation.” или “The maximum report processing jobs limit configured by your system administrator has been reached”.
Проблема заключается в том, что отчеты не освобождаются и продолжают накапливаться до достижения лимита в 75. Чтобы решить эту проблему, отчеты должны быть освобождены как можно раньше. Это звучит просто, но не так просто, как кажется. В зависимости от того, как генерируются отчеты, существует два сценария: первый - генерация PDF или Excel таблиц, и второй - использование Crystal Report Viewer. Каждый сценарий имеет разное время жизни, которое нужно учитывать при создании решения.
Решение
Нам нужно управлять двумя временами жизни отчетов: сгенерированные отчеты: PDF, Excel таблицы и Crystal Report viewer.
PDF и Excel таблицы генерируются во время запроса. Они могут быть освобождены в событии Page Unload. Crystal Report viewer немного отличается. Он должен охватывать запросы и хранится в сессии. Это делает освобождение отчетов viewer немного сложным, но не невозможным.
Освобождение Viewer в событии Page Unload не будет работать. Viewer имеет функциональность пагинации, которая запрашивает каждую новую страницу с сервера. Чтобы обойти эту проблему, мы реализовали счетчик ссылок на отчеты. Каждый раз, когда создается отчет, он сохраняется в concurrent dictionary. Когда отчет освобождается, отчет удаляется из словаря. При открытии типа отчета мы проверяем, что у пользователя еще нет открытого этого отчета, если есть, мы просто освобождаем существующий отчет и открываем новый на его месте. Другие возможности освободить отчет - это Session End (пользователь выходит из системы), Application End и при переходе со страницы отчета.
Наша внутренняя команда QA тестировала нефиксированную версию Crystal Reports. Crystal Reports падал примерно при 100 одновременных подключениях. После применения исправления наша команда QA запустила нагрузку на серверы с 750 одновременными подключениями без каких-либо проблем.
Кстати, мы столкнулись с задержкой при освобождении отчетов с несколькими подотчетами.
public static class ReportFactory
{
static readonly ConcurrentDictionary<string, ConcurrentDictionary<string, UserReport>> _sessions = new ConcurrentDictionary<string, ConcurrentDictionary<string, UserReport>>();
/// <summary>
/// Creates the report dispose on unload.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="page">The page.</param>
/// <returns>``0.</returns>
public static T CreateReportDisposeOnUnload<T>(this Page page) where T : IDisposable, new()
{
var report = new T();
DisposeOnUnload(page, report);
return report;
}
/// <summary>
/// Disposes on page unload.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="page">The page.</param>
/// <param name="instance">The instance.</param>
private static void DisposeOnUnload<T>(this Page page, T instance) where T : IDisposable
{
page.Unload += (s, o) =>
{
if (instance != null)
{
CloseAndDispose(instance);
}
};
}
/// <summary>
/// Unloads the report when user navigates away from report.
/// </summary>
/// <param name="page">The page.</param>
public static void UnloadReportWhenUserNavigatesAwayFromPage(this Page page)
{
var sessionId = page.Session.SessionID;
var pageName = Path.GetFileName(page.Request.Url.AbsolutePath);
var contains = _sessions.ContainsKey(sessionId);
if (contains)
{
var reports = _sessions[sessionId];
var report = reports.Where(r => r.Value.PageName != pageName).ToList();
foreach (var userReport in report)
{
UserReport instance;
bool removed = reports.TryRemove(userReport.Key, out instance);
if (removed)
{
CloseAndDispose(instance.Report);
}
}
}
}
/// <summary>
/// Gets the report.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns>ReportClass.</returns>
public static T CreateReportForCrystalReportViewer<T>(this Page page) where T : IDisposable, new()
{
var report = CreateReport<T>(page);
return report;
}
/// <summary>
/// Creates the report.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="page">The page.</param>
/// <returns>``0.</returns>
private static T CreateReport<T>(Page page) where T : IDisposable, new()
{
MoreThan70ReportsFoundRemoveOldestReport();
var sessionId = page.Session.SessionID;
bool containsKey = _sessions.ContainsKey(sessionId);
var reportKey = typeof(T).FullName;
var newReport = GetUserReport<T>(page);
if (containsKey)
{
//Get user by session id
var reports = _sessions[sessionId];
//check for the report, remove it and dispose it if it exists in the collection
RemoveReportWhenMatchingTypeFound<T>(reports);
//add the report to the collection
reports.TryAdd(reportKey, newReport);
//add the reports to the user key in the concurrent dictionary
_sessions[sessionId] = reports;
}
else //key does not exist in the collection
{
var newDictionary = new ConcurrentDictionary<string, UserReport>();
newDictionary.TryAdd(reportKey, newReport);
_sessions[sessionId] = newDictionary;
}
return (T)newReport.Report;
}
/// <summary>
/// Ifs the more than 70 reports remove the oldest report.
/// </summary>
private static void MoreThan70ReportsFoundRemoveOldestReport()
{
var reports = _sessions.SelectMany(r => r.Value).ToList();
if (reports.Count() > 70)
{
//order the reports with the oldest on top.
var sorted = reports.OrderByDescending(r => r.Value.TimeAdded);
//remove the oldest
var first = sorted.FirstOrDefault();
var key = first.Key;
var sessionKey = first.Value.SessionId;
if (first.Value != null)
{
//close and depose of the first report
CloseAndDispose(first.Value.Report);
var dictionary = _sessions[sessionKey];
var containsKey = dictionary.ContainsKey(key);
if (containsKey)
{
//remove the disposed report from the collection
UserReport report;
dictionary.TryRemove(key, out report);
}
}
}
}
/// <summary>
/// Removes the report if there is a report with a match type.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="reports">The reports.</param>
private static void RemoveReportWhenMatchingTypeFound<T>(ConcurrentDictionary<string, UserReport> reports) where T : IDisposable, new()
{
var key = typeof(T).FullName;
var containsKey = reports.ContainsKey(key);
if (containsKey)
{
UserReport instance;
bool removed = reports.TryRemove(key, out instance);
if (removed)
{
CloseAndDispose(instance.Report);
}
}
}
/// <summary>
/// Removes the reports for session.
/// </summary>
/// <param name="sessionId">The session identifier.</param>
public static void RemoveReportsForSession(string sessionId)
{
var containsKey = _sessions.ContainsKey(sessionId);
if (containsKey)
{
ConcurrentDictionary<string, UserReport> session;
var removed = _sessions.TryRemove(sessionId, out session);
if (removed)
{
foreach (var report in session.Where(r => r.Value.Report != null))
{
CloseAndDispose(report.Value.Report);
}
}
}
}
/// <summary>
/// Closes the and dispose.
/// </summary>
/// <param name="report">The report.</param>
private static void CloseAndDispose(IDisposable report)
{
report.Dispose();
}
/// <summary>
/// Gets the user report.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns>UserReport.</returns>
private static UserReport GetUserReport<T>(Page page) where T : IDisposable, new()
{
string onlyPageName = Path.GetFileName(page.Request.Url.AbsolutePath);
var report = new T();
var userReport = new UserReport { PageName = onlyPageName, TimeAdded = DateTime.UtcNow, Report = report, SessionId = page.Session.SessionID };
return userReport;
}
/// <summary>
/// Removes all reports.
/// </summary>
public static void RemoveAllReports()
{
foreach (var session in _sessions)
{
foreach (var report in session.Value)
{
if (report.Value.Report != null)
{
CloseAndDispose(report.Value.Report);
}
}
//remove all the disposed reports
session.Value.Clear();
}
//empty the collection
_sessions.Clear();
}
private class UserReport
{
/// <summary>
/// Gets or sets the time added.
/// </summary>
/// <value>The time added.</value>
public DateTime TimeAdded { get; set; }
/// <summary>
/// Gets or sets the report.
/// </summary>
/// <value>The report.</value>
public IDisposable Report { get; set; }
/// <summary>
/// Gets or sets the session identifier.
/// </summary>
/// <value>The session identifier.</value>
public string SessionId { get; set; }
/// <summary>
/// Gets or sets the name of the page.
/// </summary>
/// <value>The name of the page.</value>
public string PageName { get; set; }
}
}
Автор: Чак Конвей специализируется на разработке программного обеспечения и генеративном ИИ. Свяжитесь с ним в социальных сетях: X (@chuckconway) или посетите его на YouTube.