Skip to content
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

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
4 changes: 2 additions & 2 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
report
tmp/
.env
dist
.cache
.github
.github
docs
Binary file added docs/images/ttl-arct.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions docs/ttl-engine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# TTL cache engine Architecture

![](./images/ttl-arct.png)
91 changes: 91 additions & 0 deletions src/engines/TimeToLive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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 timeSeriesIndex = new HashTable();
const timeIndexInterval = 5 * 60 * 1000; // milliseconds.

let lastRunGC = Date.now();

this.add = (key, value, ttl = defaultTTL) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
benhurdavies marked this conversation as resolved.
Show resolved Hide resolved
throw Error(
'Expected ttl value. 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.getFirst();
benhurdavies marked this conversation as resolved.
Show resolved Hide resolved
const payload = { value, ttl: expireTTL, tNode };
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 expireTime or short expires is more precise,

store[hashTableProp.add](key, payload);
Comment on lines +23 to +24
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why buffe
store[hashTableProp.add](key, { value, ttl: expireTTL, tNode });

};

this.get = key => {
const payload = store[hashTableProp.get](key);
if (payload) {
const { ttl, value } = payload;
if (checkIfElementExpire(payload)) return undefined;
benhurdavies marked this conversation as resolved.
Show resolved Hide resolved
else return value;
}
return undefined;
};

this.has = key => {
return store[hashTableProp.has](key);
};

this.remove = (key) => {
if (this.has(key)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

store[hashTableProp.has](key) because for remove the expire check is not relevant

const { ttl, tNode } = store[hashTableProp.get](key);
const timeBucket = getTimeBucket(ttl);
timeBucket.remove(tNode);
store[hashTableProp.remove](key);
}
};

this.size = () => {
return store[hashTableProp.size]();
};

function getTimeBucket(expireTTL) {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 = getTimeIndex({ time: expireTTL, interval:timeIndexInterval });

if (timeSeriesIndex[hashTableProp.has](timeIndex)) {
return timeSeriesIndex[hashTableProp.get](timeIndex);
} else {
const list = new DoublyLinkedList();
timeSeriesIndex[hashTableProp.add](timeIndex, list);
return list;
}
}

function checkIfElementExpire({ ttl }) {
if (ttl < Date.now()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your wish for indentations :-) Why not if (ttl >= Date.now()) return false then no indentation needed

const timeIndex = getTimeIndex({ time: ttl, interval:timeIndexInterval });
cleanExpired(timeIndex);
return true;
}
return false;
}

function cleanExpired(timeIndex) {
const keys = timeSeriesIndex[hashTableProp.get][timeIndex];
for (key of keys) {
store[hashTableProp.remove](key);
}
}
}

// time : unix timestamp milliseconds
// interval : milliseconds (better to be factors of 60 (minutes))
function getTimeIndex({ time, interval }) {
const timeParts = parseInt(time / interval, 10);
const forwardIndexTime = timeParts * interval + interval;
return forwardIndexTime;
}

export { getTimeIndex };
export default TimeToLive;
38 changes: 38 additions & 0 deletions src/engines/TimeToLive.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import TimeToLive, { getTimeIndex } from './TimeToLive';

describe('TimeToLive (TTL) : getTimeIndex', () => {
it('should index time to next upcoming interval', () => {
const toMinute = val => val * 60 * 1000;

const date1 = new Date('2020-06-14T03:23:34');
expect(getTimeIndex({ time: date1.getTime(), interval: toMinute(5) })).toBe(
new Date('2020-06-14T03:25:00').getTime(),
);

const date2 = new Date('2020-06-14T03:43:36');
expect(
getTimeIndex({ time: date2.getTime(), interval: toMinute(10) }),
).toBe(new Date('2020-06-14T03:50:00').getTime());

const date3 = new Date('2020-06-14T03:43:36');
expect(getTimeIndex({ time: date3.getTime(), interval: toMinute(3) })).toBe(
new Date('2020-06-14T03:45:00').getTime(),
);
});
});

describe('TimeToLive', () => {
it('should have basic cache features', () => {
const ttlCache = new TimeToLive();
ttlCache.add('apple',5,1000);
ttlCache.add('orange',2,2000);
expect(ttlCache.get('apple')).toBe(5);
expect(ttlCache.has('apple')).toBe(true);
expect(ttlCache.size()).toBe(2);

ttlCache.remove('apple');
expect(ttlCache.size()).toBe(1);
expect(ttlCache.get('apple')).toBe(undefined);
expect(ttlCache.get('orange')).toBe(2);
});
});