Merge pull request #198 from Shopify/limit_resource_usage

Resource usage limits
This commit is contained in:
Florian Weingarten
2013-05-31 08:24:29 -07:00
7 changed files with 90 additions and 16 deletions
+12 -1
View File
@@ -90,6 +90,9 @@ module Liquid
def render_all(list, context) def render_all(list, context)
output = [] output = []
context.resource_limits[:render_length_current] = 0
context.resource_limits[:render_score_current] += list.length
list.each do |token| list.each do |token|
# Break out if we have any unhanded interrupts. # Break out if we have any unhanded interrupts.
break if context.has_interrupt? break if context.has_interrupt?
@@ -103,7 +106,15 @@ module Liquid
break break
end end
output << (token.respond_to?(:render) ? token.render(context) : token) token_output = (token.respond_to?(:render) ? token.render(context) : token)
context.resource_limits[:render_length_current] += (token_output.respond_to?(:length) ? token_output.length : 1)
if context.resource_limits_reached?
context.resource_limits[:reached] = true
raise MemoryError.new("Memory limits exceeded")
end
output << token_output
rescue MemoryError => e
raise e
rescue ::StandardError => e rescue ::StandardError => e
output << (context.handle_error(e)) output << (context.handle_error(e))
end end
+14 -7
View File
@@ -13,19 +13,26 @@ module Liquid
# #
# context['bob'] #=> nil class Context # context['bob'] #=> nil class Context
class Context class Context
attr_reader :scopes, :errors, :registers, :environments attr_reader :scopes, :errors, :registers, :environments, :resource_limits
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false) def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = {})
@environments = [environments].flatten @environments = [environments].flatten
@scopes = [(outer_scope || {})] @scopes = [(outer_scope || {})]
@registers = registers @registers = registers
@errors = [] @errors = []
@rethrow_errors = rethrow_errors @rethrow_errors = rethrow_errors
@resource_limits = (resource_limits || {}).merge!({ :render_score_current => 0, :assign_score_current => 0 })
squash_instance_assigns_with_environments squash_instance_assigns_with_environments
@interrupts = [] @interrupts = []
end end
def resource_limits_reached?
(@resource_limits[:render_length_limit] && @resource_limits[:render_length_current] > @resource_limits[:render_length_limit]) ||
(@resource_limits[:render_score_limit] && @resource_limits[:render_score_current] > @resource_limits[:render_score_limit] ) ||
(@resource_limits[:assign_score_limit] && @resource_limits[:assign_score_current] > @resource_limits[:assign_score_limit] )
end
def strainer def strainer
@strainer ||= Strainer.create(self) @strainer ||= Strainer.create(self)
end end
+2 -1
View File
@@ -1,6 +1,6 @@
module Liquid module Liquid
class Error < ::StandardError; end class Error < ::StandardError; end
class ArgumentError < Error; end class ArgumentError < Error; end
class ContextError < Error; end class ContextError < Error; end
class FilterNotFound < Error; end class FilterNotFound < Error; end
@@ -8,4 +8,5 @@ module Liquid
class StandardError < Error; end class StandardError < Error; end
class SyntaxError < Error; end class SyntaxError < Error; end
class StackLevelError < Error; end class StackLevelError < Error; end
class MemoryError < Error; end
end end
+4 -2
View File
@@ -23,8 +23,10 @@ module Liquid
end end
def render(context) def render(context)
context.scopes.last[@to] = @from.render(context) val = @from.render(context)
'' context.scopes.last[@to] = val
context.resource_limits[:assign_score_current] += (val.respond_to?(:length) ? val.length : 1)
''
end end
end end
+1
View File
@@ -27,6 +27,7 @@ module Liquid
def render(context) def render(context)
output = super output = super
context.scopes.last[@to] = output context.scopes.last[@to] = output
context.resource_limits[:assign_score_current] += (output.respond_to?(:length) ? output.length : 1)
'' ''
end end
end end
+8 -5
View File
@@ -14,7 +14,7 @@ module Liquid
# template.render('user_name' => 'bob') # template.render('user_name' => 'bob')
# #
class Template class Template
attr_accessor :root attr_accessor :root, :resource_limits
@@file_system = BlankFileSystem.new @@file_system = BlankFileSystem.new
class << self class << self
@@ -50,6 +50,7 @@ module Liquid
# creates a new <tt>Template</tt> from an array of tokens. Use <tt>Template.parse</tt> instead # creates a new <tt>Template</tt> from an array of tokens. Use <tt>Template.parse</tt> instead
def initialize def initialize
@resource_limits = {}
end end
# Parse source code. # Parse source code.
@@ -88,14 +89,14 @@ module Liquid
# #
def render(*args) def render(*args)
return '' if @root.nil? return '' if @root.nil?
context = case args.first context = case args.first
when Liquid::Context when Liquid::Context
args.shift args.shift
when Hash when Hash
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors) Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
when nil when nil
Context.new(assigns, instance_assigns, registers, @rethrow_errors) Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
else else
raise ArgumentError, "Expect Hash or Liquid::Context as parameter" raise ArgumentError, "Expect Hash or Liquid::Context as parameter"
end end
@@ -120,9 +121,11 @@ module Liquid
begin begin
# render the nodelist. # render the nodelist.
# for performance reasons we get a array back here. join will make a string out of it # for performance reasons we get an array back here. join will make a string out of it.
result = @root.render(context) result = @root.render(context)
result.respond_to?(:join) ? result.join : result result.respond_to?(:join) ? result.join : result
rescue Liquid::MemoryError => e
context.handle_error(e)
ensure ensure
@errors = context.errors @errors = context.errors
end end
+49
View File
@@ -71,4 +71,53 @@ class TemplateTest < Test::Unit::TestCase
assert_equal '1', t.render(assigns) assert_equal '1', t.render(assigns)
@global = nil @global = nil
end end
def test_resource_limits_render_length
t = Template.parse("0123456789")
t.resource_limits = { :render_length_limit => 5 }
assert_equal "Liquid error: Memory limits exceeded", t.render()
assert t.resource_limits[:reached]
t.resource_limits = { :render_length_limit => 10 }
assert_equal "0123456789", t.render()
assert_not_nil t.resource_limits[:render_length_current]
end
def test_resource_limits_render_score
t = Template.parse("{% for a in (1..10) %} {% for a in (1..10) %} foo {% endfor %} {% endfor %}")
t.resource_limits = { :render_score_limit => 50 }
assert_equal "Liquid error: Memory limits exceeded", t.render()
assert t.resource_limits[:reached]
t = Template.parse("{% for a in (1..100) %} foo {% endfor %}")
t.resource_limits = { :render_score_limit => 50 }
assert_equal "Liquid error: Memory limits exceeded", t.render()
assert t.resource_limits[:reached]
t.resource_limits = { :render_score_limit => 200 }
assert_equal (" foo " * 100), t.render()
assert_not_nil t.resource_limits[:render_score_current]
end
def test_resource_limits_assign_score
t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}")
t.resource_limits = { :assign_score_limit => 1 }
assert_equal "Liquid error: Memory limits exceeded", t.render()
assert t.resource_limits[:reached]
t.resource_limits = { :assign_score_limit => 2 }
assert_equal "", t.render()
assert_not_nil t.resource_limits[:assign_score_current]
end
def test_resource_limits_aborts_rendering_after_first_error
t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
t.resource_limits = { :render_score_limit => 50 }
assert_equal "Liquid error: Memory limits exceeded", t.render()
assert t.resource_limits[:reached]
end
def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set
t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}")
t.render()
assert t.resource_limits[:assign_score_current] > 0
assert t.resource_limits[:render_score_current] > 0
assert t.resource_limits[:render_length_current] > 0
end
end # TemplateTest end # TemplateTest