Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
bhngupta authored Jun 13, 2024
2 parents f1a4786 + 07fa23f commit fe3bd7d
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 126 deletions.
35 changes: 29 additions & 6 deletions include/Database.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,49 @@
#include "HashTable.h"
#include <chrono>
#include <list>
#include <string>
#include <mutex>
#include <thread>
#include <atomic>
#include <unordered_map>

class Database {
public:
Database(size_t capacity, int ttl = 600); // Default TTL of 600 seconds (10 minutes)

Database(size_t capacity, int ttl = 600);
~Database();

void set(const std::string& key, const std::string& value);
std::string get(const std::string& key);
void del(const std::string& key);
void removeExpiredEntries();

void startBackgroundThread();
void stopBackgroundThread();

private:
void evictIfNecessary();

void backgroundTask();
void purgeExpired();
void maintainCapacity();
void updateExpiration(const std::string& key);

struct CacheEntry {
std::string value;
std::chrono::steady_clock::time_point expiration;
};

size_t capacity;
int ttl; // Time to live in seconds

HashTable hashTable;
std::unordered_map<std::string, std::chrono::steady_clock::time_point> expirationMap;

std::mutex mutex_;
std::atomic<bool> running_;
std::thread backgroundThread_;
std::list<std::string> lruList;
std::unordered_map<std::string, CacheEntry> cache;
std::unordered_map<std::string, std::list<std::string>::iterator> lruMap;
size_t capacity;
int ttl; // Time to live in seconds

};

#endif // DATABASE_H
1 change: 1 addition & 0 deletions include/HashTable.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class HashTable {
std::string get(const std::string& key);
void del(const std::string& key);
bool exists(const std::string& key);
size_t size() const;

private:
std::unordered_map<std::string, std::string> table;
Expand Down
6 changes: 5 additions & 1 deletion runtest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ cd build

cmake ..

make
make

cd build

make run_tests
124 changes: 88 additions & 36 deletions src/Database.cpp
Original file line number Diff line number Diff line change
@@ -1,51 +1,76 @@
#include "Database.h"
#include <iostream>

Database::Database(size_t capacity, int ttl) : capacity(capacity), ttl(ttl) {}
Database::Database(size_t capacity, int ttl) : capacity(capacity), ttl(ttl), running_(false) {}

Database::~Database() {
stopBackgroundThread();
}

void Database::set(const std::string& key, const std::string& value) {
std::lock_guard<std::mutex> lock(mutex_);

// New expiration set => Time Now + TTL
auto now = std::chrono::steady_clock::now();
auto expiration = now + std::chrono::seconds(ttl);

// Insert or Update happens
hashTable.set(key, value);
expirationMap[key] = std::chrono::steady_clock::now() + std::chrono::seconds(ttl);
std::cout << "Set key: " << key << " with TTL: " << ttl << " seconds" << std::endl;
cache[key] = {value, expiration};

std::cout << "SET > key: " << key << std::endl;


// Update LRU cache
if (lruMap.find(key) != lruMap.end()) {
lruList.erase(lruMap[key]);
}
lruList.push_front(key);
lruMap[key] = lruList.begin();
evictIfNecessary();

maintainCapacity();
}

std::string Database::get(const std::string& key) {
auto it = expirationMap.find(key);
if (it != expirationMap.end() && std::chrono::steady_clock::now() > it->second) {
std::cout << "Key: " << key << " has expired" << std::endl;
hashTable.del(key);
expirationMap.erase(it);
if (lruMap.find(key) != lruMap.end()) {
lruList.erase(lruMap[key]);
lruMap.erase(key);
}
std::lock_guard<std::mutex> lock(mutex_);

//TODO - Need to check in registry before
//TODO - Change returns "" if not in hashtable

auto it = cache.find(key);
if (it == cache.end()) {
std::cout << "GET key: " << key << " not found" << std::endl;
return "";
}

std::string value = hashTable.get(key);
if (hashTable.exists(key)) {
auto now = std::chrono::steady_clock::now();
if (it->second.expiration <= now) {
std::cout << "GET key: " << key << " expired" << std::endl;
cache.erase(key);
lruList.erase(lruMap[key]);
lruList.push_front(key);
lruMap[key] = lruList.begin();
lruMap.erase(key);
return "";
}

std::cout << "Get key: " << key << ", value: " << value << std::endl;
auto value = it->second.value;
it->second.expiration = now + std::chrono::seconds(ttl); // Update expiration time

// Update LRU list
lruList.erase(lruMap[key]);
lruList.push_front(key);
lruMap[key] = lruList.begin();

std::cout << "GET key: " << key << " value: " << value << std::endl;
return value;
}

void Database::del(const std::string& key) {
std::cout << "Attempting to delete key: " << key << std::endl;
std::lock_guard<std::mutex> lock(mutex_);

if (hashTable.exists(key)) {
hashTable.del(key);
expirationMap.erase(key);
cache.erase(key);

if (lruMap.find(key) != lruMap.end()) {
lruList.erase(lruMap[key]);
lruMap.erase(key);
Expand All @@ -56,31 +81,58 @@ void Database::del(const std::string& key) {
}
}

void Database::removeExpiredEntries() {
void Database::startBackgroundThread() {
running_ = true;
backgroundThread_ = std::thread(&Database::backgroundTask, this);
}

void Database::stopBackgroundThread() {
running_ = false;
if (backgroundThread_.joinable()) {
backgroundThread_.join();
}
}

// || Helper Functions ||

void Database::backgroundTask() {
while (running_) {
std::this_thread::sleep_for(std::chrono::seconds(30));
{
std::lock_guard<std::mutex> lock(mutex_);
purgeExpired();
}
}
}

void Database::purgeExpired() {
auto now = std::chrono::steady_clock::now();
for (auto it = expirationMap.begin(); it != expirationMap.end();) {
if (now > it->second) {
std::string key = it->first;
std::cout << "Key: " << key << " has expired" << std::endl;
hashTable.del(key);
if (lruMap.find(key) != lruMap.end()) {
lruList.erase(lruMap[key]);
lruMap.erase(key);
for (auto it = lruList.rbegin(); it != lruList.rend(); ++it) {
auto key = *it;
auto entry = cache[key];
if (entry.expiration <= now) {
cache.erase(key);
lruMap.erase(key);
lruList.erase(std::next(it).base());
std::cout << "Removed expired key from cache: " << key << std::endl;

if (hashTable.exists(key)) {
hashTable.del(key);
std::cout << "Deleted key: " << key << std::endl;
}
it = expirationMap.erase(it);
} else {
++it;
break; // LRU list is in descending order of access time, so no need to check further
}
}
}

void Database::evictIfNecessary() {
while (lruList.size() > capacity) {
void Database::maintainCapacity() {
while (hashTable.size() > capacity) {
std::string key = lruList.back();
lruList.pop_back();
lruMap.erase(key);
hashTable.del(key);
expirationMap.erase(key);
cache.erase(key);
lruMap.erase(key);
std::cout << "Evicted key: " << key << " due to capacity limits" << std::endl;
}
}
}
4 changes: 4 additions & 0 deletions src/HashTable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ void HashTable::del(const std::string& key) {
bool HashTable::exists(const std::string& key) {
return table.find(key) != table.end();
}

size_t HashTable::size() const {
return table.size();
}
3 changes: 2 additions & 1 deletion src/ZippyClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ void printMenu() {
}

int main(int argc, char** argv) {
ZippyClient client(grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials()));

ZippyClient client(grpc::CreateChannel("0.0.0.0:50051", grpc::InsecureChannelCredentials()));

int choice;
std::string key, value, command, reply;
Expand Down
86 changes: 50 additions & 36 deletions tests/tDatabase.cpp
Original file line number Diff line number Diff line change
@@ -1,63 +1,77 @@
#include <gtest/gtest.h>
#include "Database.h"
#include <thread> // For sleep_for
#include <thread>
#include <chrono>

// Test fixture for Database class
class DatabaseTest : public ::testing::Test {
protected:
// Member variables
Database db;

// Constructor to initialize Database with capacity and a short TTL for testing
DatabaseTest() : db(3, 5) {} // Set a capacity of 3 for LRU cache and a TTL of 5 seconds for testing

// Helper functions
void SetUp() override {
db = std::make_unique<Database>(3, 10);
db->startBackgroundThread();
// Code to run before each test
}

void TearDown() override {
db->stopBackgroundThread();
db.reset();
// Code to run after each test
}

std::unique_ptr<Database> db;

};

// Test case for set and get methods
TEST_F(DatabaseTest, SetAndGet) {
db.set("key1", "value1");
EXPECT_EQ(db.get("key1"), "value1");

db.set("key2", "value2");
EXPECT_EQ(db.get("key2"), "value2");
TEST_F(DatabaseTest, SetGetTest) {
db->set("key1", "value1");
EXPECT_EQ(db->get("key1"), "value1");
}

// Test case for non-existing key
TEST_F(DatabaseTest, NonExistingKey) {
EXPECT_EQ(db.get("non_existing"), "");
// Test case for deleting a key
TEST_F(DatabaseTest, DeletionTest) {
db->set("key3", "value3");
db->del("key3");
EXPECT_EQ(db->get("key3"), ""); // Should return empty as the key has been deleted
}

// Test case for deleting a key
TEST_F(DatabaseTest, DeleteKey) {
db.set("key1", "value1");
db.del("key1");
EXPECT_EQ(db.get("key1"), "");
// Test expiration
TEST_F(DatabaseTest, ExpirationTest) {
db->set("key2", "value2");
std::this_thread::sleep_for(std::chrono::seconds(11)); // Wait for TTL to expire
EXPECT_EQ(db->get("key2"), ""); // Should return empty as the key has expired
}

// Test case for expired key
TEST_F(DatabaseTest, ExpiredKey) {
db.set("key1", "value1");
std::this_thread::sleep_for(std::chrono::seconds(6)); // Sleep for longer than the TTL
EXPECT_EQ(db.get("key1"), "");
// Test for LRU eviction
TEST_F(DatabaseTest, LRUEvictionTest) {
db->set("key1", "value1");
db->set("key2", "value2");
db->set("key3", "value3");
db->set("key4", "value4");

// As key1 is the oldest, it should be evicted
EXPECT_EQ(db->get("key1"), "");
EXPECT_EQ(db->get("key4"), "value4");
}

// Test case for eviction policy (LRU)
TEST_F(DatabaseTest, EvictionPolicy) {
db.set("key1", "value1");
db.set("key2", "value2");
db.set("key3", "value3");
db.set("key4", "value4"); // This should evict key1 (the least recently used)

EXPECT_EQ(db.get("key1"), ""); // key1 should have been evicted
EXPECT_EQ(db.get("key2"), "value2");
EXPECT_EQ(db.get("key3"), "value3");
EXPECT_EQ(db.get("key4"), "value4");
TEST_F(DatabaseTest, LRUOrderTest) {
db->set("key1", "value1");
db->set("key2", "value2");
db->set("key3", "value3");
db->get("key1");
db->set("key4", "value4");

EXPECT_EQ(db->get("key2"), ""); // key2 should be evicted
EXPECT_EQ(db->get("key1"), "value1");
EXPECT_EQ(db->get("key4"), "value4");
}

TEST_F(DatabaseTest, UpdateExpirationTimeTest) {
db->set("key4", "value4");
std::this_thread::sleep_for(std::chrono::seconds(5)); // Wait for half TTL
db->get("key4"); // Access should update expiration time
std::this_thread::sleep_for(std::chrono::seconds(6)); // Wait more than half TTL
EXPECT_EQ(db->get("key4"), "value4"); // Should still be available as TTL was reset
}
Loading

0 comments on commit fe3bd7d

Please sign in to comment.