Skip to content

Commit

Permalink
Add mutex synchronization around store read/writes. Handle "false" as…
Browse files Browse the repository at this point in the history
… values for memoization (#9)
  • Loading branch information
ritikesh committed Mar 18, 2021
1 parent b1d759e commit fc2b98f
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 109 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
/tmp/
.vscode/
*.gem
.DS_Store
.DS_Store
.byebug_history
31 changes: 21 additions & 10 deletions lib/memoize_until/store.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
class MemoizeUntil
class Store
attr_reader :_store, :_kind
attr_reader :_store, :_kind, :_mutex

def initialize(kind)
@_store = {}
@_kind = kind
@_mutex = Mutex.new
end

# returns the value from memory if already memoized for "now" for the given key
Expand All @@ -13,31 +14,37 @@ def initialize(kind)
def fetch(key, &block)
now = Time.now.public_send(_kind)
value = get(key, now)
unless value

if value.nil?
clear_all(key)
value = set(key, now, yield)
value = set(key, now, set_nil(yield))
end

unset_nil(value)
end

# add runtime keys
def add(key)
_store[key] ||= {}
_mutex.synchronize do
_store[key] ||= {}
end
end

# clears all previously memoized values for the given key
# only clears memory in the process that this code runs.
# added for supporting fetch and custom scripts
def clear_all(key)
_store[key] = {}
_mutex.synchronize do
_store[key] = {}
end
end

# clears previously memoized value for "now" for the given key
# only clears memory in the process that this code runs on.
# added for supporting custom scripts / test cases
def clear_now(key)
now = Time.now.public_send(_kind)
_store[key][now] = nil
set(key, now, nil)
end

private
Expand All @@ -53,13 +60,17 @@ def unset_nil(value)
end

def set(key, now, value)
_store[key][now] = set_nil(value)
_mutex.synchronize do
_store[key][now] = value
end
end

def get(key, now)
purpose = _store[key]
raise NotImplementedError unless purpose
purpose[now]
_mutex.synchronize do
purpose = _store[key]
raise NotImplementedError unless purpose
purpose[now]
end
end
end
end
199 changes: 101 additions & 98 deletions test/memoize_until_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,103 +2,106 @@

class MemoizeUntilTest < Minitest::Test

def setup
clear_all_values
end

def test_basic_functionality_with_thread_safety
latch = Concurrent::CountDownLatch.new(3)
waiter = Thread.new do
latch.wait()
return_val = memoize_day(:default) { "new value" }
assert_equal return_val, "hello world"
end
t1 = Thread.new do
memoize_day(:default) { "hello world" }
latch.count_down
end
t2 = Thread.new do
memoize_day(:default) { "hello world" }
latch.count_down
end
t3 = Thread.new do
memoize_day(:default) { "hello world" }
latch.count_down
end
[waiter, t1, t2, t3].each(&:join)
end

def test_exception
assert_raises MemoizeUntil::NotImplementedError do
memoize_day(:exception) { "hello world" }
end
end

def test_nil
memoize_week(:default) { nil }
return_val = memoize_week(:default) { 123 }
assert_nil return_val
end

def test_multi_add_to
MemoizeUntil.add_to(:day, :new_key)
memoize_day(:new_key) { 1000 }
MemoizeUntil.add_to(:day, :new_key)
return_val = memoize_day(:new_key) { 1 }
assert_equal return_val, 1000
end

def test_memoization_expiration
memoize_min(:default) { "hello world" }
sleep(60)
return_val = memoize_min(:default) { "hello world 2" }
assert_equal return_val, "hello world 2"
end

def test_clear_now_for
return_val = memoize_day(:default) { 1 }
assert_equal return_val, 1
MemoizeUntil.clear_now_for(:day, :default)
return_val = memoize_day(:default) { 2 }
assert_equal return_val, 2
end

private

def memoize_day(key)
MemoizeUntil.day(key) {
yield
}
end

def memoize_min(key)
MemoizeUntil.min(key) {
yield
}
end

def memoize_week(key)
MemoizeUntil.week(key) {
yield
}
end

def clear_all_values
clear_day
clear_min
clear_week
end

def clear_day
MemoizeUntil.clear_now_for(:day, :default)
end

def clear_min
MemoizeUntil.clear_now_for(:min, :default)
end

def clear_week
MemoizeUntil.clear_now_for(:week, :default)
end
def setup
clear_all_values
end

def test_basic_functionality_with_thread_safety
threads = []
1000.times do |n|
threads << Thread.new do
memoize_day(:default) { "hello world #{n}" }
end
end

# assert_nothing_raised
threads.map(&:join)

return_val = memoize_day(:default) { 1 }
assert_match /hello world/, return_val
end

def test_exception
assert_raises MemoizeUntil::NotImplementedError do
memoize_day(:exception) { "hello world" }
end
end

def test_nil
init_val = memoize_week(:default) { nil }
return_val = memoize_week(:default) { 123 }

assert_nil init_val
assert_nil return_val
end

def test_falsy
init_val = memoize_week(:default) { false }
return_val = memoize_week(:default) { 123 }

assert_equal return_val, false
assert_equal return_val, init_val
end

def test_multiple_add_to
MemoizeUntil.add_to(:day, :new_key)
memoize_day(:new_key) { 1000 }
MemoizeUntil.add_to(:day, :new_key)
return_val = memoize_day(:new_key) { 1 }
assert_equal return_val, 1000
end

def test_memoization_expiration
memoize_min(:default) { "hello world" }
sleep(60)
return_val = memoize_min(:default) { "hello world 2" }
assert_equal return_val, "hello world 2"
end

def test_clear_now_for
return_val = memoize_day(:default) { 1 }
assert_equal return_val, 1
MemoizeUntil.clear_now_for(:day, :default)
return_val = memoize_day(:default) { 2 }
assert_equal return_val, 2
end

private

def memoize_day(key)
MemoizeUntil.day(key) {
yield
}
end

def memoize_min(key)
MemoizeUntil.min(key) {
yield
}
end

def memoize_week(key)
MemoizeUntil.week(key) {
yield
}
end

def clear_all_values
clear_day
clear_min
clear_week
end

def clear_day
MemoizeUntil.clear_now_for(:day, :default)
end

def clear_min
MemoizeUntil.clear_now_for(:min, :default)
end

def clear_week
MemoizeUntil.clear_now_for(:week, :default)
end

end

0 comments on commit fc2b98f

Please sign in to comment.