Fix large for-loop collection#1071
Conversation
A large collection in a for loop results in a crash or large memory usage during render.
If a large collection is given to a for block, this may crash the render process of the template. Setting of resource_limits does not help because these are tested at a later stage. This can be prevented by adding a simple check to the Liquid::For block which makes sure that a collection size does never exceed the set render_score_limit.
|
BTW: You can checkout this branch to show the behavior for a large given collection without the fix: https://github.com/stayhero/liquid/tree/large_forloop_failing_test?files=1 |
pushrax
left a comment
There was a problem hiding this comment.
Great catch!
Long-term I think it would make sense to iterate ranges more efficiently without constructing the intermediate array. Some range math can be done for the common cases to handle reverse and offsets. This patch makes sense as a solution for now though.
Resource limit gets triggered without a nested loop in the test for a large collection range.
|
@pushrax Anything missing to merge the PR? |
|
@pushrax: ping |
|
👋 Friendly bump to see what it would take to merge. |
| score_limit = context.resource_limits.render_score_limit | ||
| if score_limit && score_limit < collection.size | ||
| context.resource_limits.render_score += collection.size | ||
| raise MemoryError.new("Memory limits exceeded".freeze) |
There was a problem hiding this comment.
The general idea here is correct, but this will cause a subtle behavior difference. The semantics are that rendering should continue until the render score is reached - meaning the first 0..RENDER_SCORE items should be rendered before the memory error.
Instead, we should do something like:
if score_limit && score_limit < collection.size
collection = collection.begin..score_limit
endAs for the For tag, we definitely should look into removing the collection = collection.to_a if collection.is_a?(Range), that's definitely a bug.
| collection = collection.to_a | ||
| end | ||
|
|
||
| limit = context.evaluate(@limit) |
There was a problem hiding this comment.
We would also need to make sure we honor the limit and offsets
When a large Range is given in a for-loop (e.g. 1..100000000000000), it may crash the Ruby process or at least eats a lot of memory and takes a significant amount of time to render the template.
I added the following testcase to template_test.rb:
The render_score_limit does not help here because it's more of a Ruby performance problem in for.rb, where the
collection = collection.to_a if collection.is_a?(Range)happens.A fix may be to check if the given Range is larger than the given render_score_limit. I'm actually not sure if there may be a better fix (e.g. checking the resource limits somewhere else) but at least it works for the given test case.