diff --git a/lib/roo/helpers/weak_instance_cache.rb b/lib/roo/helpers/weak_instance_cache.rb index 96c026a2..db26de1a 100644 --- a/lib/roo/helpers/weak_instance_cache.rb +++ b/lib/roo/helpers/weak_instance_cache.rb @@ -22,11 +22,20 @@ def instance_cache(key) unless object object = yield + ObjectSpace.define_finalizer(object, instance_cache_finalizer(key)) instance_variable_set(key, WeakRef.new(object)) end object end + + def instance_cache_finalizer(key) + proc do |object_id| + if instance_variable_defined?(key) && (ref = instance_variable_get(key)) && (!ref.weakref_alive? || ref.__getobj__.object_id == object_id) + remove_instance_variable(key) + end + end + end end end end diff --git a/spec/lib/roo/weak_instance_cache_spec.rb b/spec/lib/roo/weak_instance_cache_spec.rb new file mode 100644 index 00000000..5adcb908 --- /dev/null +++ b/spec/lib/roo/weak_instance_cache_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +if RUBY_PLATFORM == "java" + require 'java' + java_import 'java.lang.System' +end + +describe Roo::Helpers::WeakInstanceCache do + let(:klass) do + Class.new do + include Roo::Helpers::WeakInstanceCache + + def memoized_data + instance_cache(:@memoized_data) do + "Some Costly Operation #{rand(1000)}" * 1_000 + end + end + end + end + + subject do + klass.new + end + + it 'should be lazy' do + expect(subject.instance_variables).to_not include(:@memoized_data) + data = subject.memoized_data + expect(subject.instance_variables).to include(:@memoized_data) + end + + + it 'should be memoized' do + data = subject.memoized_data + expect(subject.memoized_data).to equal(data) + end + + it 'should recalculate after GC' do + expect(subject.instance_variables).to_not include(:@memoized_data) + GC.disable + subject.memoized_data && nil + expect(subject.instance_variables).to include(:@memoized_data) + + force_gc + expect(subject.instance_variables).to_not include(:@memoized_data) + GC.disable + subject.memoized_data && nil + expect(subject.instance_variables).to include(:@memoized_data) + end + + it 'must remove instance variable' do + expect(subject.instance_variables).to_not include(:@memoized_data) + GC.disable + subject.memoized_data && nil + expect(subject.instance_variables).to include(:@memoized_data) + + force_gc + expect(subject.instance_variables).to_not include(:@memoized_data) + end + + context '#inspect must not raise' do + it 'before calculation' do + expect{subject.inspect}.to_not raise_error + end + it 'after calculation' do + GC.disable + subject.memoized_data && nil + expect{subject.inspect}.to_not raise_error + expect(subject.inspect).to include("Some Costly Operation") + force_gc + end + it 'after GC' do + subject.memoized_data && nil + force_gc + expect(subject.instance_variables).to_not include(:@memoized_data) + expect{subject.inspect}.to_not raise_error + expect(subject.inspect).to_not include("Some Costly Operation") + end + end + + if RUBY_PLATFORM == "java" + def force_gc + System.gc + sleep(0.1) + end + else + def force_gc + GC.start + end + end +end \ No newline at end of file