-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Basic TTL engine #4
base: master
Are you sure you want to change the base?
Changes from all commits
df39131
f21230c
ee304d9
f406159
8bfd8c3
6309ba9
31a5af2
06f18b8
baef528
470d477
70d13ea
961de50
56bd47d
a081f42
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,6 @@ | |
report | ||
tmp/ | ||
.env | ||
dist | ||
.cache | ||
.github | ||
.github | ||
docs |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
{ | ||
"printWidth": 80, | ||
"printWidth": 100, | ||
"tabWidth": 2, | ||
"useTabs": false, | ||
"semi": true, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# TTL cache engine Architecture | ||
|
||
One of the challenges on ttl cache replacement is to clean the expired items. For smart cleaning, when we add new item it will time partitioned by ttl/expired value and put it into a time corresponded bucket. | ||
Whenever a get method called it check the element exist and check it is expired or not. If it is expired then removes the item and clean previous buckets. | ||
There is also a `runGC()` method in ttl cache. It will clean the buckets in between last cleaned time and now. | ||
TTL engine do not run `runGC()` method automatically or in an interval. | ||
We do not need to iterate or look all items for cleaning because of expired time partitioning. check below image for more information of architecture. | ||
|
||
![](./images/ttl-arct.png) |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import DefaultHashTable from '../dataStructure/HashTable'; | ||
import DoublyLinkedList from '../dataStructure/DoublyLinkedList'; | ||
import * as hashTableProp from '../hashTableSymbol'; | ||
|
||
function TimeToLive({ HashTable = DefaultHashTable, defaultTTL } = {}) { | ||
const store = new HashTable(); | ||
const timePartition = new HashTable(); | ||
const timeIndexInterval = 5 * 60 * 1000; // milliseconds. | ||
|
||
let lowestTimePartition = Date.now(); | ||
|
||
this.add = (key, value, ttl = defaultTTL) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just something we might want to consider. When we remove this ttl and only have the default ttl we have more performance optimization potential. |
||
if (!ttl || !Number.isInteger(ttl) || ttl <= 0) | ||
throw Error( | ||
'Expected ttl value (should be positive integer). ' + | ||
'you can have to mention it in add method or mention as defaultTTL at constructor', | ||
); | ||
|
||
const expireTTL = Date.now() + ttl; | ||
const bucket = getTimeBucket(expireTTL); | ||
bucket.addFirst(key); | ||
const tNode = bucket.getFirstNode(); | ||
const payload = { value, ttl: expireTTL, tNode }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ttl is misleading here. Per definition https://en.wikipedia.org/wiki/Time_to_live is a timespan. expireTTL is a timestamp. So |
||
store[hashTableProp.add](key, payload); | ||
Comment on lines
+23
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why buffe |
||
}; | ||
|
||
this.get = key => { | ||
const payload = store[hashTableProp.get](key); | ||
if (payload) { | ||
const { ttl, value } = payload; | ||
if (checkIfElementExpire({ ttl, key })) return undefined; | ||
else return value; | ||
} | ||
return undefined; | ||
Comment on lines
+29
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
}; | ||
|
||
this.has = key => { | ||
return ( | ||
store[hashTableProp.has](key) && | ||
!checkIfElementExpire({ ttl: store[hashTableProp.get](key), key }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe there is a bug in you test ;-) |
||
); | ||
}; | ||
|
||
this.remove = key => { | ||
if (this.has(key)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
const { ttl, tNode } = store[hashTableProp.get](key); | ||
const timeBucket = getTimeBucket(ttl); | ||
timeBucket.remove(tNode); | ||
store[hashTableProp.remove](key); | ||
} | ||
}; | ||
|
||
this.size = () => { | ||
return store[hashTableProp.size](); | ||
}; | ||
|
||
this.runGC = () => { | ||
const cleanTo = getBackwardTimeIndex({ time: Date.now(), interval: timeIndexInterval }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just realize maybe not for this version but for future version |
||
cleanExpiredBuckets(cleanTo); | ||
|
||
const nextCleanBucket = getForwardTimeIndex({ time: Date.now(), interval: timeIndexInterval }); | ||
cleanNotExpiredBucket(nextCleanBucket); | ||
}; | ||
|
||
function getTimeBucket(expireTTL) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the context of partitions this function should be called getTimePartition right? |
||
const timeIndex = getForwardTimeIndex({ | ||
time: expireTTL, | ||
interval: timeIndexInterval, | ||
}); | ||
|
||
if (timePartition[hashTableProp.has](timeIndex)) { | ||
return timePartition[hashTableProp.get](timeIndex); | ||
} else { | ||
const list = new DoublyLinkedList(); | ||
timePartition[hashTableProp.add](timeIndex, list); | ||
Comment on lines
+74
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. const list is actually the timePartition and what you call timePartition is either the partitionTable or timePartitions |
||
return list; | ||
} | ||
} | ||
|
||
const checkIfElementExpire = ({ ttl, key }) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What I really hate is when a function is called "check" but it is changing data. No one expects that. |
||
if (ttl < Date.now()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Your wish for indentations :-) Why not |
||
const timeIndex = getBackwardTimeIndex({ | ||
time: ttl, | ||
interval: timeIndexInterval, | ||
}); | ||
this.remove(key); | ||
cleanExpiredBuckets(timeIndex); | ||
return true; | ||
} | ||
return false; | ||
}; | ||
|
||
function cleanExpiredBucket(timeIndex) { | ||
if (timePartition[hashTableProp.has](timeIndex)) { | ||
const tNodes = timePartition[hashTableProp.get](timeIndex); | ||
for (const tNode of tNodes) { | ||
store[hashTableProp.remove](tNode.value); | ||
} | ||
timePartition[hashTableProp.remove](timeIndex); | ||
} | ||
} | ||
|
||
function cleanExpiredBuckets(tillTimeIndex) { | ||
const cleanFrom = getForwardTimeIndex({ | ||
time: lowestTimePartition, | ||
interval: timeIndexInterval, | ||
}); | ||
|
||
for (const curTimeIndex of getIndexBetween({ | ||
from: cleanFrom, | ||
to: tillTimeIndex, | ||
interval: timeIndexInterval, | ||
})) { | ||
cleanExpiredBucket(curTimeIndex); | ||
} | ||
lowestTimePartition = tillTimeIndex; | ||
} | ||
|
||
function cleanNotExpiredBucket(timeIndex) { | ||
if (timePartition[hashTableProp.has](timeIndex)) { | ||
const tNodes = timePartition[hashTableProp.get](timeIndex); | ||
for (const { value: key } of tNodes) { | ||
const { ttl } = store[hashTableProp.get](key); | ||
if (ttl < Date.now()) { | ||
store[hashTableProp.remove](key); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
// time : unix timestamp milliseconds | ||
// interval : milliseconds (better to be factors of 60 (minutes)) | ||
function getForwardTimeIndex({ time, interval }) { | ||
const timeParts = (time / interval) | 0; | ||
return timeParts * interval + interval; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why multiply with the interval? why not just timeParts + 1? |
||
} | ||
|
||
function getBackwardTimeIndex({ time, interval }) { | ||
const timeParts = (time / interval) | 0; | ||
return timeParts * interval; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you multiply with the interval? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for better debugging. |
||
} | ||
|
||
function* getIndexBetween({ from, to, interval }) { | ||
for (let i = from; i <= to; i += interval) yield i; | ||
} | ||
|
||
export { getForwardTimeIndex, getBackwardTimeIndex, getIndexBetween }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exporting function just for testing is bad |
||
export default TimeToLive; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sentence is misleading.
I would change it to:
Due to the time partitioning we do not need to iterate over all items for garbage collection which adds performance to the process. Check below image for more information of architecture.