Skip to content

Commit

Permalink
feat: Add time aware support (yahoo#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
BillDorn authored and redonkulus committed Nov 8, 2019
1 parent 7ca0e61 commit 447f6b8
Show file tree
Hide file tree
Showing 10 changed files with 3,789 additions and 583 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,18 @@ helper.readDimensions(function(err, dimensions) {
YCB Config lets you read just the dimensions that are available for you to contextualize a request that's
coming in. This can be an array of properties such as device type, language, feature bucket, or more.

## Scheduled Configs

To support scheduled configs as described in [ycb](https://github.com/yahoo/ycb) ycb-config must be set to time aware mode via option flag and the time must be passed as a special dimension of the context when in this mode.
```
let helper = new ConfigHelper({timeAware: true});
let context = req.context;
context.time = Date.now(); //{device: 'mobile', time: 1573235678929}
helper.read(bundle, config, context, callback);
```
The time value in the context should be a millisecond timestamp. To use a custom time dimension
it may specified asn an option:`new ConfigHelper({timeDimension: 'my-time-key'})`.

## License
This software is free to use under the Yahoo Inc. BSD license.
See the [LICENSE file][] for license text and copyright information.
Expand Down
156 changes: 156 additions & 0 deletions lib/cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@

/*jslint nomen:true, anon:true, node:true, esversion:6 */
'use strict';

/**
* Entry class used as map values and intrusive linked list nodes.
*/
class Entry {
constructor (key, value, setAt, expiresAt, groupId) {
this.next = null;
this.prev = null;
this.key = key;
this.value = value;
this.setAt = setAt;
this.expiresAt = expiresAt;
this.groupId = groupId;
}
}

/**
* LRU cache.
* Supported options are {max: int} which will set the max capacity of the cache.
*/
class ConfigCache {
constructor(options) {
options = options || {};
this.max = options.max;
if(!Number.isInteger(options.max) || options.max < 0) {
console.log('WARNING: no valid cache capacity given, defaulting to 100. %s', JSON.stringify(options));
this.max = 100;
}
if(this.max === 0) {
this.get = this.set = this.getTimeAware = this.setTimeAware = function(){};
}
this.size = 0;
this.map = new Map(); //key -> Entry
this.youngest = null;
this.oldest = null;
}

/**
* Set a cache entry.
* @param {string} key Key mapping to this value.
* @param {*} value Value to be cached.
* @param {number} groupId Id of the entry's group, used to lazily invalidate subsets of the cache.
*/
set(key, value, groupId) {
this.setTimeAware(key, value, 0, 0, groupId);
}

/**
* Set a time aware cache entry.
* @param {string} key Key mapping to this value.
* @param {*} value Value to be cached.
* @param {number} now Current time.
* @param {number} expiresAt Time at which entry will become stale.
* @param {number} groupId Id of the entry's group, used to lazily invalidate subsets of the cache.
*/
setTimeAware(key, value, now, expiresAt, groupId) {
var entry = this.map.get(key);
if(entry !== undefined) {
entry.value = value;
entry.setAt = now;
entry.expiresAt = expiresAt;
entry.groupId = groupId;
this._makeYoungest(entry);
return;
}
if(this.size === this.max) {
entry = this.oldest;
this.map.delete(entry.key);
entry.key = key;
entry.value = value;
entry.setAt = now;
entry.expiresAt = expiresAt;
entry.groupId = groupId;
this.map.set(key, entry);
this._makeYoungest(entry);
return;
}
entry = new Entry(key, value, now, expiresAt, groupId);
this.map.set(key, entry);
if(this.size === 0) {
this.youngest = entry;
this.oldest = entry;
this.size = 1;
return;
}
entry.next = this.youngest;
this.youngest.prev = entry;
this.youngest = entry;
this.size++;
}

/**
* Get value from the cache. Will return undefined if entry is not in cache or is stale.
* @param {string} key Key to look up in cache.
* @param {number} groupId Group id to check if value is stale.
* @returns {*}
*/
get(key, groupId) {
var entry = this.map.get(key);
if(entry !== undefined) {
if(groupId !== entry.groupId) {
return undefined; //do not clean up stale entry as we know client code will set this key
}
this._makeYoungest(entry);
return entry.value;
}
return undefined;
}

/**
* Get value from the cache with time awareness. Will return undefined if entry is not in cache or is stale.
* @param {string} key Key to look up in cache.
* @param {number} now Current time to check if value is stale.
* @param {number} groupId Group id to check if value is stale.
* @returns {*}
*/
getTimeAware(key, now, groupId) {
var entry = this.map.get(key);
if(entry !== undefined) {
if(groupId !== entry.groupId || now < entry.setAt || now >= entry.expiresAt){
return undefined; //do not clean up stale entry as we know client code will set this key
}
this._makeYoungest(entry);
return entry.value;
}
return undefined;
}

/**
* Move entry to the head of the list and set as youngest.
* @param {Entry} entry
* @private
*/
_makeYoungest(entry) {
if(entry === this.youngest) {
return;
}
var prev = entry.prev;
if(entry === this.oldest) {
prev.next = null;
this.oldest = prev;
} else {
prev.next = entry.next;
entry.next.prev = prev;
}
entry.prev = null;
this.youngest.prev = entry;
entry.next = this.youngest;
this.youngest = entry;
}
}

module.exports = ConfigCache;
Loading

0 comments on commit 447f6b8

Please sign in to comment.