Posts
Solução alternativa para o limite máximo de processamento de relatórios do Crystal Reports 13
18 de dezembro de 2013 • 6 min de leitura
Na versão do Visual Studio 2012 do Crystal Reports 13, há um limite que restringe relatórios simultâneos, incluindo sub-relatórios, a 75 relatórios em uma máquina. Isso significa que se houver 5 aplicações web em um determinado servidor, todos os relatórios abertos em todas as 5 aplicações web contam para o limite de 75 relatórios.
O erro se manifesta de diferentes formas e pode resultar nos seguintes erros: “Memória insuficiente para a operação.” ou “O limite máximo de trabalhos de processamento de relatórios configurado pelo administrador do sistema foi atingido”.
O problema é que os relatórios não são descartados e continuam se acumulando até que o limite de 75 seja atingido. Para corrigir esse problema, os relatórios devem ser descartados no momento mais cedo possível. Isso parece simples, mas não é tão direto quanto parece. Dependendo de como os relatórios são gerados, existem dois cenários: o primeiro é gerar PDFs ou planilhas do Excel, e o segundo é usar o Visualizador de Relatórios do Crystal. Cada cenário tem um tempo de vida diferente, que precisamos levar em conta ao elaborar nossa solução.
Solução
Existem dois tempos de vida de relatórios que precisamos gerenciar: relatórios gerados: PDF, Planilha do Excel e o visualizador de Relatórios do Crystal.
PDFs e Planilhas do Excel são gerados durante a solicitação. Eles podem ser descartados no evento Page Unload. O visualizador de Relatórios do Crystal é um pouco diferente. Ele precisa abranger solicitações e é armazenado em sessão. Isso torna o descarte dos relatórios do visualizador um pouco desafiador, mas não impossível.
Descartar o Visualizador no evento Page Unload não funcionará. O Visualizador tem funcionalidade de paginação que solicita cada nova página do servidor. Para contornar esse problema, implementamos um contador de referência de relatório. Cada vez que um relatório é criado, ele é armazenado em um dicionário simultâneo. Quando um relatório é descartado, o relatório é removido do dicionário. Ao abrir um tipo de relatório, verificamos se o usuário já não tem este relatório aberto; se tiver, simplesmente descartamos o relatório existente e abrimos um novo em seu lugar. Outras oportunidades para descartar o relatório são no Session End (o usuário faz logout), no Application End e ao navegar para longe da página de relatório.
Nossa equipe de QA interna testou uma versão não corrigida do Crystal Reports. O Crystal Reports falhou em torno de 100 conexões simultâneas. Após aplicar a correção, nossa equipe de QA executou um teste de carga nos servidores com 750 conexões simultâneas sem problemas.
Como nota adicional, encontramos latência ao descartar relatórios com vários sub-relatórios.
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; }
}
}
Autor: Chuck Conway é um Engenheiro de IA com quase 30 anos de experiência em engenharia de software. Ele constrói sistemas de IA práticos—pipelines de conteúdo, agentes de infraestrutura e ferramentas que resolvem problemas reais—e compartilha o que está aprendendo ao longo do caminho. Conecte-se com ele nas redes sociais: X (@chuckconway) ou visite-o no YouTube e no SubStack.