tuespetre / TuesPechkin

A .NET wrapper for the wkhtmltopdf library with an object-oriented API.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Singleton approach used, and IIS still crashes with an error

naserenho opened this issue · comments

Hello there,

I've been dealing with this IIS stopping to work issue for a while now, where I defined the converter as a static global variable in the page that I'm using to convert html to pdf. The result pdf is unstable since it sometimes crashes, sometimes it doesn't show the inside images, and sometimes it displays empty white A4 pages.

This is the error:

External component has thrown an exception Server stack trace: at TuesPechkin.WkhtmltoxBindings.wkhtmltopdf_convert(IntPtr converter) at TuesPechkin.PdfToolset.PerformConversion(IntPtr converter) at System.Runtime.Remoting.Messaging.StackBuilderSink._PrivateProcessMessage(IntPtr md, Object[] args, Object server, Object[]& outArgs) at System.Runtime.Remoting.Messaging.StackBuilderSink.SyncProcessMessage(IMessage msg) Exception rethrown at [0]: at TuesPechkin.ThreadSafeConverter.Invoke[TResult](FuncShim 1 delegate) at TuesPechkin.ThreadSafeConverter.Convert(IDocument document) at download.aspx.cs:line 593

I then tried to apply the singleton approach (code from a previous issue here), where I defined a static class in a page in my WebProject,

`public static class TuesPechkinInitializerService
{
private static string staticDeploymentPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "wkhtmltopdf");

        public static void CreateWkhtmltopdfPath()
        {
            if (Directory.Exists(staticDeploymentPath) == false)
            {
                Directory.CreateDirectory(staticDeploymentPath);
            }
        }

        public static IConverter converter =
            new ThreadSafeConverter(
                new RemotingToolset<PdfToolset>(
                    new Win32EmbeddedDeployment(
                        new StaticDeployment(staticDeploymentPath)
                    )
                )
            );
    }`

and called a function in that class to initialize the converter variable from my Application_Start in the Global.asax.
TuesPechkinInitializerService.CreateWkhtmltopdfPath();

The result is that it does run longer, but I still get random behavior like the mentioned before, including the IIS error. What is the best way to tackle this issue? Is there anything I missed in the implementation? Please I need to know.

I have a solution for you.

It's not pretty, and has some drawbacks (if you recycle the app pool or domain too often and don't have much storage you could create a denial of service by filling up the disk with temporary copies)....

BUT.... it does work reliably. We've been using it for almost 9 months now without any dramas of the dreaded "omg PDFs aren't working... need to restart the apppool".

Thus, in my experience, this is THE solution for IIS if you want it to be reliable (as it can be).

Fundamentally, our observation was that IIS stability issues seemed to stem from the AppDomain restarting, without the application pool restarting.

Solution:

  • Create a local folder C:\htmltopdf (or whatever you want), store the wkhtmltox dll here.
  • The library uses the file/path to the wkhtmltoxdll as some sort of key internally for it's copy of the dll, changing this creates a new instance.
  • Anytime the AppDomain restarts (as opposed to the app pool), or encounters a specific type of error, it leaving the library failing until you restart the app pool or process.
  • Therefore generating a brand new copy of the wkhtmltox dll on every appdomain restart (static variable) cleans it up nicely, you force this, by changing the path. Because the library will still have the old copy of the dll locked, we can't delete the old copy until a later date.
  • As a fallback, if we do somehow still get a PDF Generation fail, we then create a nice clean copy of wkhtmltopdf in case (just like when a new app domain gets created) and try again

Semi-Complete Code example below (with a fair bit of internal stuff omitted and some cleanup, so might be some mistakes)

Call StartPdf from Global.asax

public class WkHtmlToPdfService : IHtmlToPdfService
    {
       

        static IConverter converter;
        static RemotingToolset<PdfToolset> toolset;
        static StaticDeployment deployment;
        

        static string dllFolder;
        static string dllFullPath;

        public static bool StartPdf()
        {
            Cleanup();

            var existingPath = ConfigurationManager.AppSettings["WkHtmlToPdf.Path"] ?? @"C:\htmltopdf";

            dllFolder = Path.Combine(HttpContext.Current.Server.MapPath("~/App_Data/ScratchSpace/HtmlToPdfDll"), DateTime.UtcNow.Ticks.ToString());
            Directory.CreateDirectory(dllFolder);
            var existingFullPath = Path.Combine(existingPath, "wkhtmltox.dll");
            dllFullPath = Path.Combine(dllFolder, "wkhtmltox.dll");
            File.Copy(existingFullPath, dllFullPath);



            deployment = new TuesPechkin.StaticDeployment(dllFolder);
            toolset = new RemotingToolset<PdfToolset>(deployment);
            converter = new ThreadSafeConverter(toolset);


            return true;
        }

        public static void Cleanup()
        {
            try
            {

                var dirsInHtmlToPdfFolder = Directory.GetDirectories(HttpContext.Current.Server.MapPath("~/App_Data/ScratchSpace/HtmlToPdfDll"));

                foreach (var di in dirsInHtmlToPdfFolder)
                {
                    var dirName = Path.GetFileName(di);
                    bool isLong;
                    long ticks;
                    isLong = long.TryParse(dirName, out ticks);
                    //if it's not a date-folder or it's older than 2 days, should be safe to delete
                    if (!isLong || new DateTime(ticks) < DateTime.UtcNow.AddDays(-2))
                    {
                        Directory.Delete(di, true);
                    }

                }
            }
            finally
            {

            }

        }


...........

 public byte[] HtmlToPdf(string html, string headerHtml = "", string documentTitle = "", bool isPortrait = true)
{
            int maxNumberOfRetries = 3;
            int retryCount = 0;
            byte[] result = null;


            while (retryCount <= maxNumberOfRetries)
            {
                var document = new HtmlToPdfDocument()
                {
                  ....
                };



                try
                {
                    result = converter.Convert(document);
                    if (result != null)
                    {
                        break;
                    }
                }
                catch (Exception ex)
                {
                    _logger.Fatal(.......);
                    StartPdf();//restart conversion process if we get a failed pdf
                
                }
            
                retryCount++;
                

          }

return result;
}

}

I almost lost hope in checking back here lol
I somehow managed with the Singleton approach, it's stable so far...although i still got issues at the beginning but not anymore. I understand your solution, and I think the drawback won't be much of an issue in my case 👍 I'll be implementing it as an extra layer of stability...thank you so much for your reply ^_^

When i tried to use this solution, converter.Convert(document) throw exception that "Unable to find an entry point named 'wkhtmltopdf_init' in DLL 'wkhtmltox.dll'. "

When i tried to use this solution, converter.Convert(document) throw exception that "Unable to find an entry point named 'wkhtmltopdf_init' in DLL 'wkhtmltox.dll'. "

I found error, i would should use wkhtmltox.dll in tuespeckhin zip codes. I use tuespeckhin.wkhtmltox.win32.dll instead of that one.