An under-the-hood look at the Reporting REST Service Storage purpose, structure and usage.
In this blog post, I will elaborate on the storage paradigm of the Reporting REST Service. The purpose of the blog is to provide a more technical view of the specifics of the storage. This in-depth knowledge will benefit the service implementors that would learn how the storage creates, retrieves, and deletes its assets.
I have included some pros and cons of the different storage types. You will also find hints on how to troubleshoot and repair the REST Service Storage. This information would be also valuable if you decided to produce your custom storage implementation.
The Html5-based Report Viewers require a Progress Telerik Reporting REST Service to render the displayed reports. The article HTML5 Report Viewer and Telerik Reporting REST Service elaborates on this client-service relationship.
The REST Service role is to expose stateless API (Application Programming Interface), presenting the report documents as resources accessed by the report viewers who are the service’s clients. Still, the service needs to preserve the rendered document resources for a period of time to be able to serve them. It also needs to preserve additional data as to when each client was created and when it should expire, what reports the client has requested so far, etc.
All these resources get stored in a key-value store that we call Storage. The service logic generates unique resource keys to store and later retrieve resources from the Storage, as necessary. It also deletes these resources once they are not used anymore. Therefore, we may consider the stored resources as cached ones.
To implement the Reporting REST Service, you inherit the ReportsControllerBase class. This base class depends on the storage object to operate. This dependency, however, is not to a concrete implementation, but to the IStorage interface abstraction. This allows plugging in different storage implementations using the service configuration.
There are three built-in storage implementations that we provide. These are the MSSQL Server and the Redis implementations that utilize the corresponding data providers for accessing the respective database. The FileStorage implementation uses the classes in the System.IOnamespace to store the values as files on the file system. Below I will explain the advantages of each storage implementation.
The used storage and its configuration are added to the ReportServiceConfiguration object. We recommend setting up the ReportServiceConfiguration object using dependency injection in a singleton scope or within the static constructor of the implemented ReportsController. This avoids recreating the service configuration along with the ReportsController which gets created for each request.
You may find examples for configuring the ReportsController in the different .NET environments in our Documentation:
All assets are stored in the REST Service Storage as key-value pairs. The keys include random GUIDs generated during the series of requests performed for creating the assets. All keys resemble nested file paths giving structure to the keys and ensuring uniqueness. The key structure follows several patterns that depend on the type of asset.
Here is an example of a key for a PDF report document asset:
HostAppIdValue\17.0.23.118\Is\07105a131f3\Rs\b640fa04800\
Ds\2a34bd2772e\R\report\pdf
And here is the meaning of the elements in this structure. The values enclosed in curly brackets are sample values that may vary as specified:
For the built-in FileStorage, the REST Service stores each client’s assets in folders nested according to the file-path structure of the keys. Here is an example of the path to the PDF report document from the above example when kept in a FileStorage:
D:\RestStorageBasePath\HostAppIdValue\17.0.23.118\Is\07105a131f3\Rs\
b640fa04800\Ds\2a34bd2772e\R\report\pdf
The first part of the path, {D:\RestStorageBasePath} is the base storage path from the FileStorage constructor.
In the case of the built-in database Storages, the REST Service stores each client’s assets in database table rows with specific names. The diverse types of assets reside in four different tables named “tr_AppLock,” “tr_Object,” “tr_Set” and “tr_String.”
The PDF asset from the example will be stored in the table tr_Object with the key:
HostAppIdValue\17.0.23.118\Is\07105a131f3\Rs\b640fa04800\Ds\2a34bd2772e\
R\report\pdf
The request URLs also include the above asset names/identifiers, and the REST Service determines where to look for the asset in the storage based on them. If the corresponding folder or table row exists on the REST Service instance storage, the request would be successful.
Here is an example of the URL sent from the viewer to the service for exporting the same report kept in PDF format in the REST Service Storage:
http://localhost:51863/api/reports/clients/b8eabdb4225/instances/07105a131f3/
documents/b640fa048002a34bd2772e?response-content-disposition=attachment
The URL consists of the following parts:
The rest of the information kept in the storage and requested from the clients is referenced through paths, keys and URLs with similar structure.
The reports service gets requests from multiple clients in a parallel manner. But the stored resources should be written and read in a sequential manner so that their integrity is preserved.
The IStorage interface enables its client applications (the REST service in this case) to perform application locks and in this way to serialize their requests to the storage. This mechanism achieves mutually exclusive access to the stored resources. The responsible method for this functionality is IStorage.AcquireLock.
In the case of MSSQL Server and Redis storage, the AcquireLock method implementation utilizes built-in database locking mechanisms. The fact that the locks are performed on database level, outside of the report service process, allows scaling the web service by deploying it in multiple instances. All instances hit the common storage after acquiring locks as necessary to ensure the needed sequential access and with it preserving the data integrity. That is why MSSQL Server and Redis are the IStorage implementations suitable for deploying both on single instance application hosting and in Web Farms.
The FileStorage implementation, on the other hand, utilizes Mutex objects which can only synchronize the requests in the boundaries of a single machine, making it unsuitable for load balancing. Using FileStorage in such an environment would lead to race conditions and unexpected results. That is why we consider FileStorage as IStorage implementation suitable for deploying on a single instance application only. Its setup is simpler, so it may be used also in proof-of-concept solutions.
In the Web Farms scenario, called also load balancing, there are multiple instances of the same service, for example, the Telerik Reporting REST Service, installed on different server machines. Each request is directed to one of the machines by a controller.
In all cases, we recommend using database storage for load balancing. The main reason for this is the internal locking mechanisms of the databases as explained above. The FileStorage cannot be used effectively in Web Farms as explained next.
If you need to use FileStorage with multiple REST Service instances, you should consider the next two scenarios, each one subject to limitations:
Since the REST Service Storage keeps all the necessary information about the registered clients/viewers, including generated report documents, with each generated report the occupied storage space would increase. Therefore, when a client or a report expires, the Reporting Service triggers a cleanup routine deleting the related assets. You may control the assets’ expiration using the ClientSessionTimeout and ReportSharingTimeout properties of the service configuration.
If you attach a Trace Listener to the REST Service application, it logs information on deleting the expired resources from the storage—see Troubleshooting ASP.NET Core application for hints on using the Trace Listener in .NET Core/6/7 projects. The relevant logs from the stack trace look like this:
Worker rendering threads count: 8
Cache Cleanup (1/24/2023 4:05:08 PM): 'Delete expired cache assets' should be performed as of 1/24/2023 2:05:07 PM, as it is not performed since 1/1/0001 12:00:00 AM and the check period is 00:05:00
Cache Cleanup (1/24/2023 4:05:08 PM): Starting to execute 'Delete expired cache assets'
Cache Cleanup (1/24/2023 4:05:08 PM): Starting to execute 'Delete expired clients'
Cache Cleanup (1/24/2023 4:05:08 PM): 1 clients survived expiration, namely: 4f3c98b8425
Cache Cleanup (1/24/2023 4:05:08 PM): Finished executing 'Delete expired clients'; elapsed: 00:00:00.0089791
Cache Cleanup (1/24/2023 4:05:08 PM): Starting to execute 'Delete expired refreshes & instances'
Cache Cleanup (1/24/2023 4:05:08 PM): Finished executing 'Delete expired refreshes & instances'; elapsed: 00:00:00.0008378
Cache Cleanup (1/24/2023 4:05:08 PM): Finished executing 'Delete expired cache assets'; elapsed: 00:00:00.0269926
CSharp.Net6.Html5IntegrationDemo Information: 0 : Unit.DotsPerInch = 96
Telerik Reporting Assembly Information
======================================
Name: Telerik.Reporting, Version=17.0.23.118, Culture=neutral, PublicKeyToken=a9d7983dfcc261be; Location: D:\Program Files\Progress\Telerik Reporting R1 2023\Examples\CSharp\.NET 6\Html5IntegrationDemo\bin\Debug\net6.0\Telerik.Reporting.dll; TargetFramework: .NETStandard,Version=v2.0
Assembly ref successfully loaded: SpeakerObjects, Culture=neutral
Rendering thread 19 starts work.
*** ReportProcessor.ProcessReport STARTED ***
CSharp.Net6.Html5IntegrationDemo Information: 0 : SQLite x64: v3033000
*** ProcessReport #0 STARTED ***
*** ProcessReport #0 DONE in 00:00:00.3913869 ***
*** ReportProcessor.ProcessReport DONE in 00:00:00.7751045 ***
The service caches each client’s (viewer’s) resources in the Storage. If a viewer gets closed, there is no notification, and the service does not know about that.
The ClientSessionTimeout property of the ReportsController determines the time of the inactivity of a viewer that would cause its resources to expire. Periodically (each 5 minutes, as indicated also in the trace log, but not more often), the REST Service performs cleaning of the expired data.
Note that the service triggers this logic only when awakened by a request from an active client/viewer. Hence, all inactive sessions will be deleted after some time determined by the ClientSessionTimeout and the time it takes to delete the expired data, provided the service is awake.
If the service stops before the client sessions have been deleted (including before it has been awakened to delete them), the uncleaned sessions will remain in the Storage. This may cause an unnecessary increase in the space occupied by the storage.
To avoid it, you may periodically delete the storage, for example when the application hosting the REST Service is being restarted. This way all the assets related to the client utilizing the closing service will be deleted.
The storage abstraction does not provide a mechanism for cleaning the entire storage. You may implement such with custom logic or delete it manually on the raw data level using the database instruments or file operations.
If you restart the REST Service, there may be a problem if it was cleaning its cache. If the cleaning was interrupted by the restart, some of the assets may lose their integrity, leaving the corresponding client in an invalid state. In this case, after the restart and when attempting new cache cleaning, the REST Service of the restarted server would come upon the invalid client, and when iterating through its assets may fail. This may be seen in the Stack Trace as an error message like in the below log:
Cache Cleanup (5/2/2022 10:15:46 AM): ClearCache finished with exception (following):
System.ArgumentNullException: Value cannot be null. (Parameter 'source')
at System.Linq.ThrowHelper.ThrowArgumentNullException(ExceptionArgument argument)
at System.Linq.Enumerable.Select[TSource,TResult](IEnumerable`1 source, Func`2 selector)
at Telerik.Reporting.Services.Engine.PersistableCollection`1.GetEnumerator()
at Telerik.Reporting.Services.Engine.ReportEngine.ClearCache(DateTime assetsExpiredOn, DateTime preventConcurrentClearUntil)
at Telerik.Reporting.Services.Engine.ReportEngine.<>c__DisplayClass63_0.<CollectExpiredData>b__0()
Cache Cleanup (5/2/2022 10:15:46 AM): Finished executing 'Delete expired cache assets'; elapsed: 00:00:00.4234422
The fix would be to entirely delete this server cache, e.g., the storage folder in the FileStorage scenario, as seen in the configuration of the service, and then restart the REST Service. Before deleting, please consider backing up this storage and providing it to us so that we can debug and see why this happened. This would let us try to improve the storage behavior and avoid such issues in the future.
The Telerik Report Server hosts a Telerik Reporting REST Service to provide live preview of the server reports. The Report Server stores its main assets and the REST Service Storage in its own Report Server Storage. The type and details of the service are stored in the configuration file “ReportServerAdmin.config” in the installation folder of the Report Server, for example “C:\Program Files (x86)\Progress\Telerik Report Server\Telerik.ReportServer.Web.”
The Report Server Storage contains two main branches determined by their keys. The first branch key name is “TRS\1” and stores the important Report Server assets. This is all the information related to the Report Server users, report definitions, scheduled tasks, data alerts, etc. When you back up the Report Server Storage, you preserve these assets. Always backup your storage to prevent losing valuable information.
The Report Server Reporting Service storage is identical to the REST Service storage set up in a custom application. The service storage constitutes the Report Server Storage second branch. The key name for this live preview cache is structured as “{serviceId}{Telerik Reporting version}.” For example, when the Report Server is installed with the default FileStorage, the live preview storage would be in the folder “C:\Program Files (x86)\Progress\Telerik Report Server\Telerik.ReportServer.Web\Data\13bb32c4\17.0.23.118.” You may safely delete the content of this cache, which contains three subfolders named “Cs,” “Is” and “LCT” when the Report Server Web application is not running.
Telerik Reporting is a complete, easy-to-use and powerful .NET embedded reporting tool for web and desktop applications that supports: Blazor, ASP.NET Core, ASP.NET MVC, ASP.NET AJAX, HTML5/JS, Angular, React, Vue, WPF, WinForms and UWP.
Also available as a part of our Telerik DevCraft bundle, Telerik Reporting allows you to create, style, view and export rich, interactive, and reusable reports to attractively present analytical and any business data. Add reports to any business application through report viewer controls. Export the ready reports to more than 15 formats.
If you still have not tried it, you can start a free trial to take a closer look. We also provide a support service we are proud of and resources that will help you along the way.
Todor Arabadzhiev is a Technical Support Engineer in the Telerik Reporting division. His main interests, outside software engineering, include bio sciences, cycling and camping.