forked from oslabs-beta/cacheflow
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcacheflow.js
622 lines (524 loc) · 19.3 KB
/
cacheflow.js
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
const fs = require('fs');
const redis = require('redis');
const { promisify } = require('util');
/*
----------------------------------------------------------------------------
TEST FUNCTION: testMsg(){}
Test to make sure npm package is connected
*/
exports.cacheflowTestMsg = function () {
console.log('This is a test message from cacheflow');
return 'This is a test message from cacheflow';
};
/*
GLOBAL VARIABLES
client: connection to redis
globalLocalThreshold: default threshold initialized from configObj.local.globalThreshold
*/
let client;
let globalLocalThreshold;
/*
----------------------------------------------------------------------------
INITIALIZE CACHE FUNCTION: initCache(){}
If user wants to cache they must initialize the cache locations by using these.
Create base files: localMetricsStorage.json and globalMetrics.json
totalNumberOfRequests: total number of all requests
averageNumberOfCalls: average number of requests per resolver
numberOfUncachedRequests: total number of uncached requests
numberOfCachedRequests: total number of cached requests
totalTimeSaved: total amount of time saved by caching in ms
averageUncachedLatency: average length of uncached query per resolver
averageCachedLatency: average length of cached query per resolver
totalUncachedElapsed: total request latency uncached
totalCachedElapsed: total request latency cached
globalAverageCallSpan: average time between resolver calls
uniqueResolvers: total number of resolvers called
sizeOfDataRedis: total amount of data saved in redis in bytes
sizeOfDataLocal: total amount of data saved locally in bytes
averageSizeOfDataLocal: average amount of data saved locally per resolver
averageCacheThreshold: 0
If user specified to intialize local storage, localStorage.json is created
If user specified to intialize redis storage, client is created and connected
Data cleaning interval is initialized
*/
exports.initCache = async function (configObj) {
if (!fs.existsSync('cacheflowSrc')) {
fs.mkdirSync('cacheflowSrc');
}
fs.writeFileSync('cacheflowSrc/localMetricsStorage.json', '{}');
fs.writeFileSync(
'cacheflowSrc/globalMetrics.json',
JSON.stringify({
totalNumberOfRequests: 0,
averageNumberOfCalls: 0,
numberOfUncachedRequests: 0,
numberOfCachedRequests: 0,
totalTimeSaved: 0,
averageUncachedLatency: 0,
averageCachedLatency: 0,
totalUncachedElapsed: 0,
totalCachedElapsed: 0,
globalAverageCallSpan: 0,
uniqueResolvers: 0,
sizeOfDataRedis: 0,
sizeOfDataLocal: 0,
averageSizeOfDataLocal: 0,
averageCacheThreshold: 0,
})
);
if (configObj.local) {
fs.writeFileSync(`cacheflowSrc/localStorage.json`, '{}');
globalLocalThreshold = configObj.local.globalThreshold / 1000;
}
if (configObj.redis) {
client = redis.createClient({
host: configObj.redis.host,
port: configObj.redis.port,
password: configObj.redis.password,
});
client.on('error', (err) => {
throw new Error('ERROR CONNECTING TO REDIS');
});
}
setInterval(() => {
clean();
}, configObj.local.checkExpire * 1000 || 10000);
};
/*
-------------------------------------------------------------
CACHE FUNCTION: cache(cachedConfig: Object that is passed in by user, info, callback){}
If cacheConfig is incorrect throw error
If cacheConfig.location is local call cachLocal
If cacheConfig.location is redis call cacheRedis
*/
exports.cache = function (cacheConfig = {}, info, callback) {
const startDate = Date.now();
if (typeof cacheConfig !== 'object' || Array.isArray(cacheConfig))
throw new Error('Config object is invalid');
if (cacheConfig.location === 'local') {
return cacheLocal(cacheConfig, info, callback, startDate);
}
if (cacheConfig.location === 'redis') {
return cacheRedis(cacheConfig, info, callback, startDate);
}
};
/*
-------------------------------------------------------------
LOCAL CACHE FUNCTION: cacheLocal() {}
If resolver was a mutation call mutateLocal
If resolver already in local cache call localFound
If resolver was not in local cache call localNotFound
*/
async function cacheLocal(cacheConfig, info, callback, startDate) {
const metrics = fsRead('cacheflowSrc/localMetricsStorage.json');
if (cacheConfig.mutate) {
if (!metrics[cacheConfig.mutate]) {
throw new Error('Data does not exist in local cache');
}
return mutateLocal(cacheConfig, callback);
}
const parsedData = fsRead('cacheflowSrc/localStorage.json');
if (parsedData[info.path.key]) {
return localFound(cacheConfig, info, startDate, parsedData);
} else {
return localNotFound(cacheConfig, info, callback, startDate, parsedData);
}
}
/*
-------------------------------------------------------------
MUTATE LOCAL FUNCTION: mutateLocal() {}
Update localStorage for the resolver the mutation was called on
Call mutationMetrics with the resolver name
Return data from the callback
*/
async function mutateLocal(cacheConfig, callback) {
const parsedData = fsRead('cacheflowSrc/localStorage.json');
const dataBack = await callback();
parsedData[cacheConfig.mutate] = {
data: dataBack,
expire: Date.now() + cacheConfig.maxAge * 1000,
};
fsWrite('cacheflowSrc/localStorage.json', parsedData);
mutationMetrics(cacheConfig.mutate, dataBack);
return parsedData[cacheConfig.mutate].data;
}
/*
-------------------------------------------------------------
LOCAL FOUND FUNCTION: localFound() {}
Read and parse localStorage.json
Time stamp and log latency
Update expiration date
Call metrics with cachedLatency
Update cache and return cached data
*/
function localFound(cacheConfig, info, startDate, parsedData) {
const currentTime = Date.now();
const requestLatencyCached = currentTime - startDate;
parsedData[info.path.key].expire = currentTime + cacheConfig.maxAge * 1000;
metrics({ cachedLatency: requestLatencyCached }, info);
const globalMetrics = fsRead('cacheflowSrc/globalMetrics.json');
globalMetrics.numberOfCachedRequests++;
globalMetrics.totalCachedElapsed += requestLatencyCached;
globalMetrics.averageCachedLatency =
globalMetrics.totalCachedElapsed / globalMetrics.numberOfCachedRequests;
fsWrite('cacheflowSrc/globalMetrics.json', globalMetrics);
fsWrite('cacheflowSrc/localStorage.json', parsedData);
return parsedData[info.path.key].data;
}
/*
-------------------------------------------------------------
LOCAL NOT FOUND FUNCTION: localNotFound() {}
Run callback to generate data
Time stamp
Add new data to parsed object
Log metrics
Cache new data and return new data
Threshold defaults to global variable globalLocalThreshold unless user has specific
threshold for that resolver on resolver's cacheConfig object
Smartcache gets called to see if data should be cached or not
*/
async function localNotFound(
cacheConfig,
info,
callback,
startDate,
parsedData
) {
const resolverName = info.path.key;
const returnData = await callback();
const currentTime = Date.now();
const requestLatencyUncached = currentTime - startDate;
let localMetrics = fsRead('cacheflowSrc/localMetricsStorage.json');
let threshold;
let inMetricCheck = false;
if (!localMetrics[resolverName]) {
inMetricCheck = true;
metrics(
{
uncachedLatency: requestLatencyUncached,
returnData,
storedLocation: 'local',
},
info
);
}
localMetrics = fsRead('cacheflowSrc/localMetricsStorage.json');
cacheConfig.threshold
? (threshold = cacheConfig.threshold / 1000)
: (threshold = globalLocalThreshold);
const globalMetrics = fsRead('cacheflowSrc/globalMetrics.json');
const allCalls = localMetrics[resolverName].allCalls;
const numberCalls = localMetrics[resolverName].numberOfCalls;
let frequency;
allCalls.length === 1
? (frequency = 0)
: (frequency = numberCalls / (allCalls[allCalls.length - 1] - allCalls[0]));
let smartCacheValue = null;
if (inMetricCheck === false) {
smartCacheValue = smartCache(localMetrics, globalMetrics, resolverName);
}
if (frequency >= threshold || smartCacheValue) {
parsedData[resolverName] = {
data: returnData,
expire: currentTime + cacheConfig.maxAge * 1000,
};
globalMetrics.numberOfCachedRequests++;
fsWrite('cacheflowSrc/globalMetrics.json', globalMetrics);
fsWrite('cacheflowSrc/localStorage.json', parsedData);
return returnData;
} else {
if (inMetricCheck === false) {
metrics(
{
uncachedLatency: requestLatencyUncached,
returnData,
storedLocation: 'local',
},
info
);
}
const globalMetrics = fsRead('cacheflowSrc/globalMetrics.json');
globalMetrics.numberOfUncachedRequests++;
globalMetrics.totalUncachedElapsed += requestLatencyUncached;
globalMetrics.averageUncachedLatency =
globalMetrics.totalUncachedElapsed /
globalMetrics.numberOfUncachedRequests;
fsWrite('cacheflowSrc/globalMetrics.json', globalMetrics);
}
return returnData;
}
/*
-------------------------------------------------------------
SMART CACHE FUNCTION: smartCache() {}
Uses number of calls, time between calls and size of data to make one comparison variable value
Uses the value variable from above and compares it to average data for all resolvers
If value is greater than the average threshold value for all resolvers return true, else return false
*/
const smartCache = (metricsData, globalMetrics, resolverName) => {
const defaultThreshold = 1;
let numberCalls =
(metricsData[resolverName].numberOfCalls -
globalMetrics.averageNumberOfCalls) /
globalMetrics.averageNumberOfCalls;
let temp;
metricsData[resolverName].averageCallSpan === 'Insufficient Data'
? (temp = 10000)
: (temp = metricsData[resolverName].averageCallSpan);
let callSpan = metricsData[resolverName].averageCallSpan;
callSpan <= 0 ? (callSpan = 5000) : null;
let dataSize =
(metricsData[resolverName].dataSize -
globalMetrics.averageSizeOfDataLocal) /
300;
const value = numberCalls + (1 / (0.004 * temp)) * 0.92 + dataSize * 0.17;
if (value > defaultThreshold * 0.97) {
globalMetrics.averageCacheThreshold =
(defaultThreshold + value) /
(globalMetrics.totalNumberOfRequests === 0
? 1
: globalMetrics.totalNumberOfRequests);
metricsData[resolverName].cacheThreshold = value;
fsWrite('cacheflowSrc/localMetricsStorage.json', metricsData);
fsWrite('cacheflowSrc/globalMetrics.json', globalMetrics);
return true;
}
fsWrite('cacheflowSrc/globalMetrics.json', globalMetrics);
return false;
};
/*
-------------------------------------------------------------
MUTATE REDIS FUNCTION: redisMutate() {}
New data is generated from callback
Set new mutated data to redis store with new expiration date
Call mutationMetrics to update metrics
Return new mutated data
*/
async function redisMutate(cacheConfig, callback, startDate) {
const returnData = await callback();
client.set(cacheConfig.mutate, JSON.stringify(returnData));
client.expire(cacheConfig.mutate, cacheConfig.maxAge);
mutationMetrics(cacheConfig.mutate, returnData);
return returnData;
}
/*
-------------------------------------------------------------
CACHE REDIS FUNCTION: cacheRedis() {}
Must promisify redis client.get function
If user is mutating data, run reisMutate function
Else see if data is in redis store or not, if not cache it and set metrics, else update expiration date and call metrics
*/
async function cacheRedis(cacheConfig, info, callback, startDate) {
const getAsync = promisify(client.get).bind(client);
const resolverName = info.path.key;
let redisData;
let responseTime;
if (cacheConfig.mutate) {
return redisMutate(cacheConfig, callback, startDate);
}
await getAsync(resolverName).then(async (res) => {
if (res === null) {
const returnData = await callback();
client.set(resolverName, JSON.stringify(returnData));
client.expire(resolverName, cacheConfig.maxAge);
redisData = returnData;
responseTime = Date.now() - startDate;
metrics(
{
uncachedLatency: responseTime,
storedLocation: 'redis',
returnData,
},
info
);
} else {
redisData = JSON.parse(res);
client.expire(resolverName, cacheConfig.maxAge);
responseTime = Date.now() - startDate;
metrics({ cachedLatency: responseTime }, info);
}
});
return redisData;
}
/*
-------------------------------------------------------------
MUTATE METRICS FUNCTION: mutationMetrics() {}
Update metrics about size of data size of specific resolver after a mutation
Update metrics about size of data size in global cache after a mutation
*/
function mutationMetrics(mutateName, data) {
const jsonLocal = fsRead('cacheflowSrc/localMetricsStorage.json');
const jsonGlobal = fsRead('cacheflowSrc/globalMetrics.json');
const oldSize = jsonLocal[mutateName].dataSize;
const newSize = sizeOf(data);
jsonLocal[mutateName].dataSize = newSize;
jsonGlobal.sizeOfDataLocal += newSize - oldSize;
fsWrite('cacheflowSrc/localMetricsStorage.json', jsonLocal);
fsWrite('cacheflowSrc/globalMetrics.json', jsonGlobal);
}
/*
-------------------------------------------------------------
METRICS FUNCTION: metrics() {}
If resolver in cache call localMetricsUpdate
If resolver not in cache call setLocalMetric
Always call globalMetrics
*/
async function metrics(resolverData, info) {
let parsedMetrics = fsRead('cacheflowSrc/localMetricsStorage.json');
if (parsedMetrics[info.path.key]) {
await localMetricsUpdate(resolverData, info, parsedMetrics);
} else {
await setLocalMetric(resolverData, info, parsedMetrics);
}
await globalMetrics(resolverData, info, parsedMetrics);
}
/*
-------------------------------------------------------------
SET LOCAL METRICS FUNCTION: setLocalMetric() {}
Update localMetricsStorage with new resolver
firstCall: timestamp from first call
allCalls: array of timestamps from calls
numberOfCalls: total number of calls for resolver
averageCallSpan: average time between calls
uncachedCallTime: length of uncached query
cachedCallTime: length of cached query
dataSize: size of data
storedLocation: where the data is stored
*/
function setLocalMetric(resolverData, info, parsedMetrics) {
globalMetricsParsed = fsRead('cacheflowSrc/globalMetrics.json');
parsedMetrics[info.path.key] = {
firstCall: Date.now(),
allCalls: [Date.now()],
numberOfCalls: 1,
averageCallSpan: 'Insufficient Data',
uncachedCallTime: resolverData.uncachedLatency,
cachedCallTime: null,
dataSize: sizeOf(resolverData.returnData),
storedLocation: resolverData.storedLocation,
cacheThreshold: null,
};
fsWrite('cacheflowSrc/localMetricsStorage.json', parsedMetrics);
resolverData.storedLocation === 'local'
? (globalMetricsParsed.sizeOfDataLocal += sizeOf(resolverData.returnData))
: null;
fsWrite('cacheflowSrc/globalMetrics.json', globalMetricsParsed);
}
/*
-------------------------------------------------------------
LOCAL METRICS UPDATE FUNCTION: cacheRedis() {}
Updates allCalls to be array with only last ten calls to resolver
Updates averageCallSpan to be length of time between last call and tenth call ago
Increments numberOfCalls by one
Sets cached call time equal to how long the cached request took
*/
function localMetricsUpdate(resolverData, info, parsedMetrics) {
const resolverName = info.path.key;
const date = Date.now();
let allCalls = parsedMetrics[resolverName].allCalls;
allCalls.push(date);
allCalls.length > 10 ? allCalls.shift() : allCalls;
if (resolverData.uncachedLatency) {
parsedMetrics[resolverName].uncachedCallTime = resolverData.uncachedLatency;
}
parsedMetrics[resolverName].averageCallSpan =
(date - allCalls[0]) / allCalls.length;
parsedMetrics[resolverName].numberOfCalls += 1;
parsedMetrics[resolverName].cachedCallTime = resolverData.cachedLatency;
fsWrite('cacheflowSrc/localMetricsStorage.json', parsedMetrics);
}
/*
-------------------------------------------------------------
GLOBAL METRICS FUNCTION: globalMetrics() {}
Increments totalNumberOfRequests by one
Increments totalTimeSaved by the difference between the cached and uncached requests for that resolver
Updates amount of data saved locally
*/
function globalMetrics(resolverData, info, parsedMetrics) {
const resolverName = info.path.key;
const numOfResolvers = Object.keys(parsedMetrics).length;
let globalMetricsParsed = fsRead('cacheflowSrc/globalMetrics.json');
globalMetricsParsed.totalNumberOfRequests++;
globalMetricsParsed.averageNumberOfCalls =
globalMetricsParsed.totalNumberOfRequests / numOfResolvers;
globalMetricsParsed.totalTimeSaved +=
parsedMetrics[resolverName].uncachedCallTime -
parsedMetrics[resolverName].cachedCallTime;
globalMetricsParsed.uniqueResolvers = numOfResolvers;
globalMetricsParsed.averageSizeOfDataLocal =
globalMetricsParsed.sizeOfDataLocal / numOfResolvers;
let globalAvgCallSpan = 0;
for (const item in parsedMetrics) {
globalAvgCallSpan += parsedMetrics[item].averageCallSpan;
}
globalMetricsParsed.globalAverageCallSpan =
globalAvgCallSpan / globalMetricsParsed.uniqueResolvers;
fsWrite('cacheflowSrc/globalMetrics.json', globalMetricsParsed);
}
/*
-------------------------------------------------------------
CLEAN STORAGE FUNCTION: clean() {}
Checks if any data stored locally is set to expire, deletes it from localStorage if its expire property is greater than Date.now()
Updates local metrics for that resolver and global metrics
*/
function clean() {
const dateNow = Date.now();
let parsedData = fsRead('cacheflowSrc/localStorage.json');
let parsedGlobalData = fsRead('cacheflowSrc/globalMetrics.json');
let parsedLocalData = fsRead('cacheflowSrc/localMetricsStorage.json');
let sizeOfDeletedDataLocal = 0;
for (let resolver in parsedData) {
if (dateNow > parsedData[resolver].expire) {
sizeOfDeletedDataLocal += parsedLocalData[resolver].dataSize;
parsedLocalData[resolver].dataSize = 0;
delete parsedData[resolver];
}
}
if (client) {
client.info((req, res) => {
res.split('\n').map((line) => {
if (line.match(/used_memory:/)) {
parsedGlobalData.sizeOfDataRedis = parseInt(line.split(':')[1]);
parsedGlobalData.sizeOfDataLocal -= sizeOfDeletedDataLocal;
fsWrite('cacheflowSrc/globalMetrics.json', parsedGlobalData);
}
});
});
}
fsWrite('cacheflowSrc/localStorage.json', parsedData);
fsWrite('cacheflowSrc/localMetricsStorage.json', parsedLocalData);
}
/*
-------------------------------------------------------------
FS FUNCTIONS:
fsRead(){}
fsWrite(){}
*/
function fsRead(fileName) {
const data = fs.readFileSync(`${fileName}`, 'utf-8');
const json = JSON.parse(data);
return json;
}
function fsWrite(fileName, data) {
fs.writeFileSync(`${fileName}`, JSON.stringify(data), (err) => {
if (err) throw new Error(err);
});
}
/*
-------------------------------------------------------------
DATA SIZE FUNCTION: sizeOf() {}
Returns an estimated size of input data in bytes
*/
const typeSizes = {
undefined: () => 0,
boolean: () => 4,
number: () => 8,
string: (item) => 2 * item.length,
object: (item) =>
!item
? 0
: Object.keys(item).reduce(
(total, key) => sizeOf(key) + sizeOf(item[key]) + total,
0
),
};
const sizeOf = (value) => typeSizes[typeof value](value);