diff --git a/Rakefile b/Rakefile index ced1c990..4a72846d 100644 --- a/Rakefile +++ b/Rakefile @@ -3,7 +3,7 @@ Bundler::GemHelper.install_tasks require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:base_spec) do |task| - task.pattern = 'spec/multi_json_spec.rb' + task.pattern = 'spec/multi_json_spec.rb,spec/options_cache_spec.rb' end namespace :adapters do diff --git a/lib/multi_json/options_cache.rb b/lib/multi_json/options_cache.rb index caa62eb5..05bf59d8 100644 --- a/lib/multi_json/options_cache.rb +++ b/lib/multi_json/options_cache.rb @@ -9,7 +9,21 @@ def reset def fetch(type, key) cache = instance_variable_get("@#{type}_cache") - cache.key?(key) ? cache[key] : cache[key] = yield + cache.key?(key) ? cache[key] : write(cache, key, &Proc.new) + end + + private + + # Normally MultiJson is used with a few option sets for both dump/load + # methods. When options are generated dynamically though, every call would + # cause a cache miss and the cache would grow indefinitely. To prevent + # this, we just reset the cache every time the number of keys outgrows + # 1000. + MAX_CACHE_SIZE = 1000 + + def write(cache, key) + cache.clear if cache.length >= MAX_CACHE_SIZE + cache[key] = yield end end end diff --git a/spec/options_cache_spec.rb b/spec/options_cache_spec.rb new file mode 100644 index 00000000..bc3dbebe --- /dev/null +++ b/spec/options_cache_spec.rb @@ -0,0 +1,20 @@ +require "spec_helper" + +describe MultiJson::OptionsCache do + before { described_class.reset } + + it "doesn't leak memory" do + described_class::MAX_CACHE_SIZE.succ.times do |i| + described_class.fetch(:dump, :key => i) do + { :foo => i } + end + + described_class.fetch(:load, :key => i) do + { :foo => i } + end + end + + expect(described_class.instance_variable_get(:@dump_cache).length).to eq(1) + expect(described_class.instance_variable_get(:@load_cache).length).to eq(1) + end +end