-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathKVStore.java
More file actions
175 lines (154 loc) · 5.17 KB
/
Copy pathKVStore.java
File metadata and controls
175 lines (154 loc) · 5.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package com.sayandip.kvstore;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* Small in-memory key-value store with two eviction paths:
*
* - LRU: once the store is at capacity, the least-recently-used entry
* is dropped to make room for a new one.
* - TTL: entries can carry an expiry. A read of an expired key is a
* miss (lazy expiry), and a background sweep also purges expired
* entries proactively (active expiry) so dead keys nobody ever reads
* again don't sit in memory forever.
*
* Lookups go through CustomHashMap (key -> Node), and each Node also
* sits in a doubly linked list that tracks recency order. That
* combination gives O(1) get/put/delete while still supporting O(1)
* "move to front" on every access -- the same trick as the classic LRU
* Cache problem, just with TTL and a real backing map layered on top.
*/
public class KVStore<K, V> {
private final int capacity;
private final CustomHashMap<K, Node<K, V>> map;
private final ReentrantLock lock = new ReentrantLock();
private Node<K, V> head; // most recently used
private Node<K, V> tail; // least recently used
private final ScheduledExecutorService sweeper;
public KVStore(int capacity) {
this(capacity, true);
}
public KVStore(int capacity, boolean enableActiveExpirySweep) {
if (capacity <= 0) throw new IllegalArgumentException("capacity must be positive");
this.capacity = capacity;
this.map = new CustomHashMap<>();
if (enableActiveExpirySweep) {
sweeper = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "kvstore-ttl-sweeper");
t.setDaemon(true);
return t;
});
sweeper.scheduleAtFixedRate(this::sweepExpired, 1, 1, TimeUnit.SECONDS);
} else {
sweeper = null;
}
}
public void put(K key, V value) {
put(key, value, -1);
}
/** ttlSeconds < 0 means "no expiry". */
public void put(K key, V value, long ttlSeconds) {
long expiresAt = ttlSeconds < 0 ? -1 : System.currentTimeMillis() + ttlSeconds * 1000;
lock.lock();
try {
Node<K, V> existing = map.get(key);
if (existing != null) {
existing.value = value;
existing.expiresAtMillis = expiresAt;
moveToFront(existing);
return;
}
Node<K, V> node = new Node<>(key, value, expiresAt);
map.put(key, node);
addToFront(node);
if (map.size() > capacity) {
evictLRU();
}
} finally {
lock.unlock();
}
}
public V get(K key) {
lock.lock();
try {
Node<K, V> node = map.get(key);
if (node == null) return null;
if (node.isExpired()) {
removeNode(node);
map.remove(key);
return null;
}
moveToFront(node);
return node.value;
} finally {
lock.unlock();
}
}
public void delete(K key) {
lock.lock();
try {
Node<K, V> node = map.remove(key);
if (node != null) removeNode(node);
} finally {
lock.unlock();
}
}
public int size() {
lock.lock();
try {
return map.size();
} finally {
lock.unlock();
}
}
public void shutdown() {
if (sweeper != null) sweeper.shutdownNow();
}
// ---- internal doubly linked list bookkeeping (recency order) ----
private void addToFront(Node<K, V> node) {
node.prev = null;
node.next = head;
if (head != null) head.prev = node;
head = node;
if (tail == null) tail = node;
}
private void moveToFront(Node<K, V> node) {
if (head == node) return;
removeNode(node);
addToFront(node);
}
private void removeNode(Node<K, V> node) {
if (node.prev != null) node.prev.next = node.next;
else head = node.next;
if (node.next != null) node.next.prev = node.prev;
else tail = node.prev;
node.prev = null;
node.next = null;
}
private void evictLRU() {
if (tail == null) return;
K evictedKey = tail.key;
removeNode(tail);
map.remove(evictedKey);
}
// O(number of expired entries near the tail) per sweep -- fine at
// demo scale; a production version would use a min-heap keyed by
// expiry instead of scanning, worth mentioning as the next step.
private void sweepExpired() {
lock.lock();
try {
Node<K, V> curr = tail;
while (curr != null) {
Node<K, V> prevNode = curr.prev;
if (curr.isExpired()) {
removeNode(curr);
map.remove(curr.key);
}
curr = prevNode;
}
} finally {
lock.unlock();
}
}
}