Skip to content

Commit 1d869ed

Browse files
raakella1Ravi Nagarjun Akella
authored andcommitted
Fix bugs in Simple Cache (eBay#263)
* Fix bugs in Simple Cache * use try_lock to delete the evicted entry from hashmap on a best effort basis * fix bugs in the do_evict logic * add commenst to explain the LRU cache functionality better --------- Co-authored-by: Ravi Nagarjun Akella <raakella1@$HOSTNAME>
1 parent 6008c4f commit 1d869ed

File tree

5 files changed

+153
-4
lines changed

5 files changed

+153
-4
lines changed

include/sisl/cache/evictor.hpp

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ typedef ValueEntryBase CacheRecord;
2828

2929
class Evictor {
3030
public:
31+
typedef std::function< bool(const CacheRecord&) > eviction_cb_t;
32+
using can_evict_cb_t = eviction_cb_t;
33+
34+
// struct to hold the eviction callbacks for each record family
35+
// can_evict_cb: called before eviction to check if the record can be evicted.
36+
// post_eviction_cb: called after eviction to do any cleanup. If this returns false, the record is reinserted.
37+
// and we try to evict the next record.
38+
struct RecordFamily {
39+
Evictor::eviction_cb_t can_evict_cb{nullptr};
40+
Evictor::eviction_cb_t post_eviction_cb{nullptr};
41+
};
3142
using do_evict_cb_t = std::function< bool(const CacheRecord&) >;
3243

3344
struct RecordFamily {
@@ -43,9 +54,13 @@ class Evictor {
4354
Evictor& operator=(Evictor&&) noexcept = delete;
4455
virtual ~Evictor() = default;
4556

57+
uint32_t register_record_family(RecordFamily record_family) {
4658
uint32_t register_record_family(RecordFamily family) {
4759
uint32_t id{0};
4860
std::unique_lock lk(m_reg_mtx);
61+
while (id < m_eviction_cbs.size()) {
62+
if (m_eviction_cbs[id].first == false) {
63+
m_eviction_cbs[id] = std::make_pair(true, record_family);
4964
while (id < m_record_families.size()) {
5065
if (m_record_families[id].registered == false) {
5166
m_record_families[id] = std::move(family);
@@ -58,26 +73,30 @@ class Evictor {
5873
return 0;
5974
}
6075

61-
void unregister_record_family(uint32_t record_type_id) {
76+
void unregister_record_family(const uint32_t record_type_id) {
6277
std::unique_lock lk(m_reg_mtx);
78+
m_eviction_cbs[record_type_id] = std::make_pair(false, RecordFamily{});
6379
m_record_families[record_type_id].registered = false;
6480
m_record_families[record_type_id].do_evict_cb = nullptr;
6581
}
6682

6783
virtual bool add_record(uint64_t hash_code, CacheRecord& record) = 0;
6884
virtual void remove_record(uint64_t hash_code, CacheRecord& record) = 0;
69-
virtual bool record_accessed(uint64_t hash_code, CacheRecord& record) = 0;
85+
virtual void record_accessed(uint64_t hash_code, CacheRecord& record) = 0;
7086
virtual void record_resized(uint64_t hash_code, const CacheRecord& record, uint32_t old_size) = 0;
7187

7288
int64_t max_size() const { return m_max_size; }
7389
uint32_t num_partitions() const { return m_num_partitions; }
90+
const eviction_cb_t& can_evict_cb(const uint32_t record_id) const { return m_eviction_cbs[record_id].second.can_evict_cb; }
91+
const eviction_cb_t& post_eviction_cb(const uint32_t record_id) const { return m_eviction_cbs[record_id].second.post_eviction_cb; }
7492
const do_evict_cb_t& do_evict_cb(uint32_t record_id) const { return m_record_families[record_id].do_evict_cb; }
7593

7694
private:
7795
int64_t m_max_size;
7896
uint32_t m_num_partitions;
7997

8098
std::mutex m_reg_mtx;
99+
std::array< std::pair< bool /*registered*/, RecordFamily >, CacheRecord::max_record_families() > m_eviction_cbs;
81100
std::array< RecordFamily, CacheRecord::max_record_families() > m_record_families;
82101
};
83102
} // namespace sisl

include/sisl/cache/lru_evictor.hpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ class LRUEvictor : public Evictor {
4949

5050
void record_resized(uint64_t hash_code, const CacheRecord& record, uint32_t old_size) override;
5151

52+
// for testing purpose
53+
int64_t filled_size() {
54+
int64_t filled_size{0};
55+
for (uint32_t i{0}; i < num_partitions(); ++i) {
56+
filled_size += m_partitions[i].filled_size();
57+
}
58+
return filled_size;
59+
}
60+
5261
private:
5362
typedef list<
5463
ValueEntryBase,
@@ -82,6 +91,12 @@ class LRUEvictor : public Evictor {
8291
bool record_accessed(CacheRecord& record);
8392
void record_resized(const CacheRecord& record, uint32_t old_size);
8493

94+
// for testing purpose
95+
int64_t filled_size() {
96+
std::unique_lock guard{m_list_guard};
97+
return m_filled_size;
98+
}
99+
85100
private:
86101
bool find_evict_candidates(uint32_t record_fid, uint32_t needed_size);
87102
bool will_fill(uint32_t new_size) const { return ((m_filled_size + new_size) > m_max_size); }

include/sisl/cache/range_cache.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class RangeCache {
4343
bind_this(RangeCache< K >::on_hash_operation, 4))},
4444
m_can_evict_cb{std::move(evict_cb)},
4545
m_per_value_size{per_val_size} {
46-
m_evictor->register_record_family(std::move(evict_cb));
46+
m_evictor->register_record_family(Evictor::RecordFamily{.can_evict_cb = evict_cb});
4747
}
4848

4949
~RangeCache() { m_evictor->unregister_record_family(m_record_family_id); }

include/sisl/cache/simple_hashmap.hpp

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class SimpleHashMap {
7373
bool upsert(const K& key, const V& value);
7474
bool get(const K& input_key, V& out_val);
7575
bool erase(const K& key, V& out_val);
76+
bool try_erase(const K& key);
7677
bool update(const K& key, auto&& update_cb);
7778
bool upsert_or_delete(const K& key, auto&& update_or_delete_cb);
7879
K record_to_key(const ValueEntryBase& record);
@@ -269,9 +270,33 @@ class SimpleHashBucket {
269270
static void access_cb(const SingleEntryHashNode< V >& node, const K& key, const V& value, hash_op_t op) {
270271
SimpleHashMap< K, V >::call_access_cb((const ValueEntryBase&)node, key, value, op);
271272
}
273+
274+
bool erase_unsafe(const K& input_key, V& out_val, bool call_access_cb) {
275+
SingleEntryHashNode< V >* n = nullptr;
276+
277+
auto it = m_list.begin();
278+
for (auto itend{m_list.end()}; it != itend; ++it) {
279+
const K k = SimpleHashMap< K, V >::extractor_cb()(it->m_value);
280+
if (input_key > k) {
281+
break;
282+
} else if (input_key == k) {
283+
n = &*it;
284+
break;
285+
}
286+
}
287+
288+
if (n) {
289+
if (call_access_cb) { access_cb(*n, input_key, hash_op_t::DELETE); }
290+
out_val = n->m_value;
291+
m_list.erase(it);
292+
delete n;
293+
return true;
294+
}
295+
return false;
296+
}
272297
};
273298

274-
///////////////////////////////////////////// RangeHashMap Definitions ///////////////////////////////////
299+
///////////////////////////////////////////// SimpleHashMap Definitions ///////////////////////////////////
275300
template < typename K, typename V >
276301
SimpleHashMap< K, V >::SimpleHashMap(uint32_t nBuckets, const key_extractor_cb_t< K, V >& extract_cb,
277302
kv_access_cb_t< K, V > access_cb) :

src/cache/tests/test_simple_cache.cpp

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,96 @@ TEST_F(SimpleCacheTest, RandomData) {
187187
m_cache_misses, (100 * (double)m_cache_misses) / cache_ops);
188188
}
189189

190+
TEST_F(SimpleCacheTest, MultiThreaded) {
191+
const auto num_threads = 20;
192+
LOGINFO("INFO: Do random read/write operations on all chunks for {} threads", num_threads);
193+
std::vector< std::thread > threads;
194+
for (uint32_t i{0}; i < num_threads; ++i) {
195+
threads.emplace_back([this]() {
196+
for (uint32_t j{0}; j < 20000; ++j) {
197+
const uint32_t id = g_re() % m_total_keys;
198+
const op_t op = s_cast< op_t >(g_re() % 3);
199+
std::shared_ptr< Entry > e = std::make_shared< Entry >(0);
200+
switch (op) {
201+
case op_t::READ:
202+
m_cache->get(id, e);
203+
break;
204+
case op_t::WRITE:
205+
m_cache->insert(std::make_shared< Entry >(id, fmt::format("test{}", j)));
206+
break;
207+
case op_t::REMOVE:
208+
m_cache->remove(id, e);
209+
break;
210+
}
211+
}
212+
});
213+
}
214+
for (auto& t : threads) { t.join(); }
215+
}
216+
217+
TEST(SimpleCacheSize, TriggerEvict) {
218+
uint32_t num_partitions = 10;
219+
uint32_t max_nodes_per_partition = 3;
220+
uint32_t cache_size = g_val_size * num_partitions * max_nodes_per_partition;
221+
std::shared_ptr< Evictor > evictor = std::make_unique< LRUEvictor >(cache_size, num_partitions);
222+
auto simple_cache = std::make_unique< SimpleCache< uint32_t, std::shared_ptr< Entry > > >(
223+
evictor, // Evictor to evict used entries
224+
10000, // Total number of buckets
225+
g_val_size, // Value size
226+
[](const std::shared_ptr< Entry >& e) -> uint32_t { return e->m_id; }, // Method to extract key
227+
nullptr // Method to prevent eviction
228+
);
229+
auto* evictor_ptr = dynamic_cast<LRUEvictor*>(evictor.get());
230+
uint32_t num_iters = num_partitions * max_nodes_per_partition * 1000;
231+
for(uint32_t i = 0; i < num_iters; i++) {
232+
ASSERT_TRUE(simple_cache->insert(std::make_shared< Entry >(i, fmt::format("test{}", i))));
233+
ASSERT_LE(evictor_ptr->filled_size(), cache_size);
234+
}
235+
uint32_t cache_hits{0};
236+
for(uint32_t i = 0; i < num_iters; i++) {
237+
std::shared_ptr< Entry > e = std::make_shared< Entry >(0);
238+
if(simple_cache->get(i, e)) {
239+
++cache_hits;
240+
}
241+
}
242+
}
243+
244+
TEST(SimpleCacheSize, MultithreadedEviction) {
245+
uint32_t num_partitions = 10;
246+
uint32_t max_nodes_per_partition = 3;
247+
uint32_t cache_size = g_val_size * num_partitions * max_nodes_per_partition;
248+
std::shared_ptr< Evictor > evictor = std::make_unique< LRUEvictor >(cache_size, num_partitions);
249+
auto simple_cache = std::make_unique< SimpleCache< uint32_t, std::shared_ptr< Entry > > >(
250+
evictor, // Evictor to evict used entries
251+
10000, // Total number of buckets
252+
g_val_size, // Value size
253+
[](const std::shared_ptr< Entry >& e) -> uint32_t { return e->m_id; }, // Method to extract key
254+
nullptr // Method to prevent eviction
255+
);
256+
auto* evictor_ptr = dynamic_cast<LRUEvictor*>(evictor.get());
257+
uint32_t num_iters = num_partitions * max_nodes_per_partition * 1000;
258+
std::vector< std::thread > threads;
259+
const auto num_writers = 10;
260+
for (uint32_t i{0}; i < num_writers; ++i) {
261+
threads.emplace_back([&simple_cache, &evictor_ptr, num_iters]() {
262+
for (uint32_t j{0}; j < num_iters; ++j) {
263+
simple_cache->insert(std::make_shared< Entry >(j, fmt::format("test{}", j)));
264+
ASSERT_LE(evictor_ptr->filled_size(), evictor_ptr->max_size());
265+
}
266+
});
267+
}
268+
const auto num_readers = 10;
269+
for (uint32_t i{0}; i < num_readers; ++i) {
270+
threads.emplace_back([&simple_cache, num_iters]() {
271+
for (uint32_t j{0}; j < num_iters; ++j) {
272+
std::shared_ptr< Entry > e = std::make_shared< Entry >(0);
273+
simple_cache->get(j, e);
274+
}
275+
});
276+
}
277+
for (auto& t : threads) { t.join(); }
278+
}
279+
190280
SISL_OPTIONS_ENABLE(logging, test_simplecache)
191281
SISL_OPTION_GROUP(test_simplecache,
192282
(cache_size_mb, "", "cache_size_mb", "cache size in mb",

0 commit comments

Comments
 (0)