axelclark
How to handle max concurrency for ChromicPDF?
I’m using ChromicPDF in my application to allow a user to download a pdf. In my application, I have a button they can click that sends a request to my controller which generates the pdf with ChromicPDF and sends it back to the user with send_download from the controller.
ChomicPDF docs state:
To increase or limit the number of concurrent workers, you can pass pool configuration to the supervisor. Please note that this is a non-queueing session pool. If you intend to max it out, you will need a job queue as well.
I was able to generate the error in testing:
** (ChromicPDF.Browser.ExecutionError) Caught EXIT signal from NimblePool.checkout!/4
** (EXIT) time out
This means that your operation was unable to acquire a worker from the pool
within 5000ms, as all workers are currently occupied.
You're experiencing this error randomly under load. This would indicate that
the number of concurrent print jobs exceeds the total number of workers in
the pool, so that all workers are occupied.
To fix this, you need to increase your resources, e.g. by increasing the number
of workers with the `session_pool: [size: ...]` option.
However, please be aware that while ChromicPDF (by virtue of the underlying
NimblePool worker pool) does perform simple queueing of worker checkouts,
it is not suitable as a proper job queue. If you expect to peaks in your load
leading to a high level of concurrent use of your PDF printing component,
a job queue like Oban will provide a better experience.
The error message suggests Oban. However, I’m not sure Oban is the best option for a couple reasons:
- Oban isn’t designed for the caller to block awaiting the result of the job. However, it does have the concept of a Notifier which can be used to return the pdf to the controller process to send back to the user.
- I would prefer not to rebuild the
connandassignswithin the job because I’m reusing some functions in my controller. But I also don’t want to pass the results of ChromicPDF.Template.source_and_options/1 to the job directly.
The other options I’ve looked at are:
Any suggestions for the best option to queue the pdf printing work when I don’t want the work to be done in the background?
Marked As Solved
axelclark
I upgraded to ChromicPDF version 1.14 and updated the :session_pool :timeout and :checkout_timeout to match what I had in poolboy. ChromicPDF and NimblePool handle my concurrency test just like poolboy.
For anyone interested in the updated ChromicPDF config options, check out the session pool docs.
Also Liked
akoutmos
Currently on mobile, so I can add more info when I am in front of a computer. I make heavy use of ChromicPDF in eaglemms.com so that customers can generate invoices. I think I used to have issues similar to what you are describing so I put poolboy in front of ChromicPDF and haven’t had any issues. I also do something similar to what you are describing where a controller generates the PDF on demand versus generating and storing it in S3 or something and poolboy+ChromicPDF has no problem keeping up (at least for my current customer load).
sorentwo
maltoe
Hi, author of ChromicPDF here.
I’m currently scratching my head a bit as the accepted solution in this thread does not make sense to me (putting a worker pool in front of a worker pool), yet two people independently report good experience with it
- I wonder if this points to an obscure bug in Chromic, or merely to an opportunity to improve its documentation.
@axelclark Let me first address your original post. I believe the docs you cited led to some confusion, rather than being helpful. Especially this line:
Please note that this is a non-queueing session pool.
This is definitely misleading. I should have written that it is not a persistently queueing session pool. The NimblePool library used in Chromic’s SessionPool does in fact queue “worker checkout” commands (by virtue of OTP message boxes & genserver). But its queue isn’t persistent, as in, if you overwhelm your system, worker checkouts will timeout and requests will be dropped; which leads to the exception you have posted. That’s also perfectly fine depending on your use-case, if you expect only few concurrent requests, or if you’re fine with the occassional failure when you exceed the pool concurrency, there’s nothing you need to do about it at all
.
In other words, what I meant to say in the docs is this: If you expect peaks in concurrent demand (= greater than max throughput of your system) and you are required to handle them gracefully, you must begin to write requests down for later and asynchronously process them (e.g. with a job queue like Oban, backed by a database table, and so on). This is actually not specific to ChromicPDF in any way, just generally job processing. And vice versa, if your scenario does not have peaks or you don’t care about them, that’s 100% cool, too, and you don’t need any of this.
I probably should just ditch that statement fromt the docs.
Now, with regards to putting poolboy in front of it: I’m honestly curious how that would change anything in your test setup? You should now get a poolboy exception instead of ChromicPDF’s when you overwhelm your system. Even the default checkout timeout (think, “max queue wait time”) is the same, I believe, at 5 seconds. Question goes to @akoutmos , too, of course.
Fun fact: ChromicPDF used to depend on poolboy before v0.6.0. Which is when I realized that a process pool in front of a single connection process is pointless. NimblePool eliminates the worker processes and only pools “resources”.
maltoe
@axelclark pushed a PR adding the checkout_timeout option [#276] Make checkout timeout configurable by maltoe · Pull Request #278 · bitcrowd/chromic_pdf · GitHub
reviews welcome, especially wrt. the changes in the docs. Do you think it’s more clear now?
axelclark
Check out the Elixir School article on poolboy for an overview on poolboy:
Why use Poolboy?
Let’s think of a specific example for a moment. You are tasked to build an application for saving user profile information to the database. If you’ve created a process for every user registration, you would create an unbounded number of connections. At some point the number of those connections can exceed the capacity of your database server. Eventually your application can get timeouts and various exceptions.
The solution is to use a set of workers (processes) to limit the number of connections instead of creating a process for every user registration. Then you can easily avoid running out of your system resources.
Then for my use case, in the worker, I swapped out :math.sqrt(x) in the article for ChromicPDF.print_to_pdf/2. ChromicPDF.print_to_pdf/2 is the function that uses chromium so I want to limit and queue the processes that can access chromium at any one time.
def handle_call({:square_root, x}, _from, state) do
IO.puts("process #{inspect(self())} calculating square root of #{x}")
Process.sleep(1000)
{:reply, :math.sqrt(x), state}
end







