
Visual Studio 2012版のCrystal Reports 13には、サブレポートを含む同時実行レポートをマシン全体で75レポートまでに制限するしきい値があります。これは、特定のサーバーに5つのWebアプリケーションがある場合、すべての5つのWebアプリケーション全体で開かれたすべてのレポートが75レポート制限にカウントされることを意味します。
このエラーはさまざまな形で現れ、「操作に十分なメモリがありません」や「システム管理者によって設定された最大レポート処理ジョブ制限に達しました」などのエラーが発生する可能性があります。
問題は、レポートが破棄されず、75の制限に達するまで蓄積し続けることです。この問題を修正するには、可能な限り早い段階でレポートを破棄する必要があります。これは単純に聞こえますが、見た目ほど簡単ではありません。レポートの生成方法によって2つのシナリオがあります:1つ目はPDFやExcelスプレッドシートの生成、2つ目はCrystal Report Viewerの使用です。各シナリオには異なるライフタイムがあり、ソリューションを作成する際にこれを考慮する必要があります。
解決策
管理する必要がある2つのレポートライフタイムがあります:生成されたレポート(PDF、Excelスプレッドシート)とCrystal Report Viewerです。
PDFとExcelスプレッドシートはリクエスト中に生成されます。これらはPage Unloadイベントで破棄できます。Crystal Report Viewerは少し異なります。リクエストをまたがる必要があり、セッションに保存されます。これにより、ビューアーレポートの破棄は少し困難になりますが、不可能ではありません。
Page UnloadイベントでViewerを破棄することはうまくいきません。Viewerにはページング機能があり、各新しいページをサーバーにリクエストします。この問題を回避するために、レポート参照カウンターを実装しました。レポートが作成されるたびに、同時実行辞書に保存されます。レポートが破棄されると、レポートは辞書から削除されます。レポートのタイプを開く際に、ユーザーがすでにこのレポートを開いていないかチェックし、開いている場合は、既存のレポートを単純に破棄して、その場所に新しいレポートを開きます。レポートを破棄する他の機会は、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; }
}
}
著者:Chuck Conwayはソフトウェアエンジニアリングと生成AIを専門としています。ソーシャルメディアで彼とつながりましょう:X (@chuckconway) または YouTube をご覧ください。