├── LICENSE ├── README.md ├── __tests__ └── cacheflowTest.js ├── cacheflow.js ├── metricsTerminal.js └── package.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cacheflowQL 2 | 3 | ## What is cacheflowQL? 4 | 5 | CacheflowQL is an npm package with complex caching algorithms that provide developers deep insights into their GraphQL queries and metrics about their cached data, allowing for in-depth effective runtime and query analysis. 6 | 7 | ## Who is cacheflowQL for? 8 | 9 | CacheflowQL is for any developer looking for a lightweight way to cache data on GraphQL servers and receive in-depth metrics concerning the perfomance of their application in order to optimize run-time by utilizing server-side and Redis caching solutions. 10 | 11 | ## Installing cacheflowQL 12 | 13 | Download our NPM Package [Here](https://www.npmjs.com/package/cacheflowql) 14 | 15 | 1. Install your npm dependencies: run `npm install cacheflowql` in your terminal 16 | 17 | ## Require cacheflowQL in your server file 18 | 19 | ```js 20 | const { initCache, cache } = require('cacheflowql'); 21 | ``` 22 | 23 | ## Set up initcache 24 | 25 | Local: 26 | 27 | 1. checkExpire: Determines the interval that cacheflow checks for expired data 28 | 2. globalThreshold: A threshold value that users can set to prevent caching until input value is met 29 | 30 | Redis: 31 | 32 | 1. To connect to a redis store provide the host ip, port, and password 33 | 34 | ```js 35 | initCache({ 36 | local: { 37 | checkExpire: 10, 38 | //How often to check if data should be deleted, in seconds 39 | 40 | globalThreshold: 10, 41 | //How often data needs to be requested to be cached, in seconds 42 | }, 43 | redis: { 44 | host: '127.0.0.1', 45 | port: '6379', 46 | password: <'redis store password'> 47 | }, 48 | }); 49 | ``` 50 | 51 | ## Choose which of your resolvers you want to cache 52 | 53 | Wrap resolver within the callback function and return data from inside callback function 54 | 55 | ```js 56 | 57 | resolver(parent, args, ctx, info) { 58 | return cache( 59 | { location: 'local', maxAge: 60, smartCache: true }, 60 | info, 61 | function callback() { return data }; 62 | ); 63 | }, 64 | ``` 65 | 66 | ## Set up cacheConfig object 67 | 68 | ```js 69 | configObj = { 70 | location: 'local', 71 | //either 'local' or 'redis' 72 | maxAge: 60, 73 | //How long data will remain in local cache, in seconds 74 | smartCache: true, 75 | //boolean, determine if smart cache should be enabled 76 | }; 77 | ``` 78 | 79 | ### location 80 | 81 | - Set cache location; either local or on redis. Local and/or redis must be initiated using initCache function if either is being used. 82 | 83 | ### maxAge 84 | 85 | - How long data will remain in local cache. After maxAge (in seconds) is passed, data is marked as expired and will be deleted. 86 | 87 | ### smartCache 88 | 89 | - If you want to incorporate the smartCache capabilities of cacheflowQL you need to include a parameter in your cacheConfig object called smartCache and set it equal to true. Smartcache will prevent caching until thresholds are met. Thresholds are established by comparing metrics from specific resolvers to average metrics for all of the user's cached data. 90 | 91 | ### Mutations 92 | 93 | - If you want the cache function to work with mutations you need to wrap mutation resolvers in cache function as well. In the cache config object include a parameter 'mutate' whose value is equal to the name of the query resolver you want to have update. 94 | 95 | ```js 96 | resolver(parent, args, context, info) { 97 | return cache( 98 | { location: 'redis', maxAge: 10, mutate: 'name of resolver to mutate' }, 99 | info, 100 | function callback() { 101 | return data; 102 | } 103 | ); 104 | }, 105 | ``` 106 | 107 | ## Other inputs for cache function 108 | 109 | The other two parameters in the cache function are info and a callback. 110 | 111 | 1. The info parameter is the info parameter from the resolver itself 112 | 2. The callback parameter will be whatever was in your resolver before using cacheflowQL 113 | 114 | Simply return the call to cache with your three input parameters and you are set to cache! 115 | 116 | ```js 117 | return cache(cacheConfigObject, info, callback); 118 | ``` 119 | 120 | ## Terminal Commands 121 | 122 | To run the terminal commands run `node node_modules/cacheflowql/metricsTerminal.js`. The terminal will then ask whether you want to view local or global metrics. 123 | If you want to see data about all data cached using the cache function input 'Global Metrics.' If you want to see data about a specific resolver simply enter the name of the resolver. 124 | 125 | Screen Shot 2021-07-19 at 12 04 50 PM 126 | 127 | Screen Shot 2021-07-19 at 12 10 06 PM 128 | 129 | ## Next Steps 130 | 131 | We’re off to a great start but are always looking to add new features! Send your comments, questions, and suggestions to cacheflowql@gmail.com or simply fork our repo, checkout to a feature branch and get to work improving our product. Happy Caching! 132 | 133 | cacheflowQL is a beta product developed under OSLabs 134 | -------------------------------------------------------------------------------- /__tests__/cacheflowTest.js: -------------------------------------------------------------------------------- 1 | const { 2 | testMsg, 3 | initCache, 4 | cache, 5 | cacheLocal, 6 | createClient, 7 | } = require('../cacheflow.js'); 8 | const fs = require('fs'); 9 | const redis = require('redis'); 10 | 11 | describe('cacheflowql', () => { 12 | xdescribe('test message', () => { 13 | it('should return string: This is a test message from cacheflow', () => { 14 | expect(testMsg()).toEqual('This is a test message from cacheflow'); 15 | }); 16 | }); 17 | 18 | xdescribe('init cache local', () => { 19 | beforeAll(() => { 20 | initCache({ 21 | local: { 22 | checkExpire: 1, 23 | globalThreshold: 100, 24 | }, 25 | }); 26 | }); 27 | 28 | it('should initialize an empty object to localMetricsStorage.json', () => { 29 | const localMetrics = fs.readFileSync('localMetricsStorage.json', 'utf-8'); 30 | expect(localMetrics).toBe('{}'); 31 | }); 32 | it('should initialize a global metrics object to globalMetrics.json', () => { 33 | const defaultGlobalMetrics = { 34 | totalNumberOfRequests: 0, 35 | averageNumberOfCalls: 0, 36 | numberOfUncachedRequests: 0, 37 | numberOfCachedRequests: 0, 38 | totalTimeSaved: 0, 39 | averageUncachedLatency: 0, 40 | averageCachedLatency: 0, 41 | totalUncachedElapsed: 0, 42 | totalCachedElapsed: 0, 43 | globalAverageCallSpan: 0, 44 | uniqueResolvers: 0, 45 | sizeOfDataRedis: 0, 46 | sizeOfDataLocal: 0, 47 | averageSizeOfDataLocal: 0, 48 | averageCacheThreshold: 0, 49 | }; 50 | const globalMetrics = fs.readFileSync('globalMetrics.json', 'utf-8'); 51 | expect(globalMetrics).toBe(JSON.stringify(defaultGlobalMetrics)); 52 | }); 53 | it('should initialize a local storage file if cachConfig.local exists', () => { 54 | const LocalStorageData = fs.readFileSync(`localStorage.json`, 'utf-8'); 55 | expect(LocalStorageData).toBe('{}'); 56 | }); 57 | it('should delete data from localStorage once maxAge is reached', async () => { 58 | const localStorage = fs.readFileSync('localStorage.json', 'utf-8'); 59 | jest.useFakeTimers(); 60 | let i = 0; 61 | while (i < 3) { 62 | await cache( 63 | { location: 'local', maxAge: 0.0 }, 64 | { path: { key: 'hello' } }, 65 | function () { 66 | return 1; 67 | } 68 | ); 69 | i++; 70 | } 71 | let localStorageWithData = fs.readFileSync('localStorage.json', 'utf-8'); 72 | await new Promise((r) => setTimeout(r, 2000)); 73 | let localStorageAfterClean = fs.readFileSync( 74 | 'localStorage.json', 75 | 'utf-8' 76 | ); 77 | 78 | expect(localStorage).toBe(localStorageAfterClean); 79 | expect(localStorage).not.toBe(localStorageWithData); 80 | }); 81 | }); 82 | 83 | describe('init cache redis', () => { 84 | xit('should throw an error if incorrect port', async () => { 85 | let client = await createClient({ 86 | redis: { 87 | host: '127.0.0.1', 88 | port: '1111', 89 | }, 90 | }); 91 | await new Promise((r) => setTimeout(r, 4500)); 92 | expect(client.connected).toBe(false); 93 | }); 94 | 95 | xit('should throw an error if incorrect host', async () => { 96 | let client = await createClient({ 97 | redis: { 98 | host: '127.1.1.1', 99 | port: '6379', 100 | }, 101 | }); 102 | await new Promise((r) => setTimeout(r, 4500)); 103 | expect(client.connected).toBe(false); 104 | }); 105 | 106 | it('should connect to redis if correct host and port and redis is set up with correct port and host', async () => { 107 | let client = await createClient({ 108 | redis: { 109 | host: '127.0.0.1', 110 | port: '6379', 111 | }, 112 | }); 113 | await new Promise((r) => setTimeout(r, 4500)); 114 | expect(client.connected).toBe(true); 115 | client.end(true); 116 | }); 117 | }); 118 | 119 | xdescribe('cache function', () => { 120 | describe('determine cache local', () => { 121 | it('should fire cacheLocal if user specified', async () => { 122 | expect( 123 | await cache( 124 | { location: 'local' }, 125 | { path: { key: 'hello' } }, 126 | function () { 127 | return 1; 128 | } 129 | ) 130 | ).toEqual(1); 131 | }); 132 | it('should cache data to localStorage.json if threshold triggered', async () => { 133 | for (let i = 0; i < 5; i++) { 134 | await cache( 135 | { location: 'local' }, 136 | { path: { key: 'hello' } }, 137 | function () { 138 | return 1; 139 | } 140 | ); 141 | } 142 | 143 | expect(fs.readFileSync('localStorage.json', 'utf-8')).toEqual( 144 | '{"hello":{"data":1,"expire":null}}' 145 | ); 146 | }); 147 | }); 148 | 149 | xdescribe('determine cache redis', () => { 150 | it('should fire cacheRedis if user specified', async () => { 151 | expect( 152 | await cache( 153 | { location: 'redis' }, 154 | { path: { key: 'hello' } }, 155 | function () { 156 | return 1; 157 | } 158 | ) 159 | ).toEqual(1); 160 | }); 161 | it('should cache data to localStorage.json if threshold triggered', async () => { 162 | for (let i = 0; i < 5; i++) { 163 | await cache( 164 | { location: 'local' }, 165 | { path: { key: 'hello' } }, 166 | function () { 167 | return 1; 168 | } 169 | ); 170 | } 171 | 172 | expect(client.get('hello')).toEqual(true); 173 | }); 174 | }); 175 | }); 176 | 177 | xdescribe('Metrics', () => { 178 | describe('Global Metrics', () => { 179 | it('should log global metrics to globalMetrics.json', () => { 180 | cache({ location: 'redis' }, { path: { key: 'hello' } }, function () { 181 | return 1; 182 | }); 183 | 184 | expect(fs.readFileSync('globalMetrics.json', 'utf-8')).toEqual(''); 185 | }); 186 | }); 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /cacheflow.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const redis = require('redis'); 3 | const { promisify } = require('util'); 4 | 5 | /* 6 | ---------------------------------------------------------------------------- 7 | TEST FUNCTION: testMsg(){} 8 | 9 | Test to make sure npm package is connected 10 | 11 | */ 12 | 13 | exports.cacheflowTestMsg = function () { 14 | console.log('This is a test message from cacheflow'); 15 | return 'This is a test message from cacheflow'; 16 | }; 17 | 18 | /* 19 | GLOBAL VARIABLES 20 | 21 | client: connection to redis 22 | globalLocalThreshold: default threshold initialized from configObj.local.globalThreshold 23 | */ 24 | 25 | let client; 26 | let globalLocalThreshold; 27 | 28 | /* 29 | ---------------------------------------------------------------------------- 30 | INITIALIZE CACHE FUNCTION: initCache(){} 31 | 32 | If user wants to cache they must initialize the cache locations by using these. 33 | 34 | Create base files: localMetricsStorage.json and globalMetrics.json 35 | 36 | totalNumberOfRequests: total number of all requests 37 | averageNumberOfCalls: average number of requests per resolver 38 | numberOfUncachedRequests: total number of uncached requests 39 | numberOfCachedRequests: total number of cached requests 40 | totalTimeSaved: total amount of time saved by caching in ms 41 | averageUncachedLatency: average length of uncached query per resolver 42 | averageCachedLatency: average length of cached query per resolver 43 | totalUncachedElapsed: total request latency uncached 44 | totalCachedElapsed: total request latency cached 45 | globalAverageCallSpan: average time between resolver calls 46 | uniqueResolvers: total number of resolvers called 47 | sizeOfDataRedis: total amount of data saved in redis in bytes 48 | sizeOfDataLocal: total amount of data saved locally in bytes 49 | averageSizeOfDataLocal: average amount of data saved locally per resolver 50 | averageCacheThreshold: 0 51 | 52 | If user specified to intialize local storage, localStorage.json is created 53 | If user specified to intialize redis storage, client is created and connected 54 | Data cleaning interval is initialized 55 | 56 | */ 57 | 58 | exports.initCache = async function (configObj) { 59 | if (!fs.existsSync('cacheflowSrc')) { 60 | fs.mkdirSync('cacheflowSrc'); 61 | } 62 | fs.writeFileSync('cacheflowSrc/localMetricsStorage.json', '{}'); 63 | fs.writeFileSync( 64 | 'cacheflowSrc/globalMetrics.json', 65 | JSON.stringify({ 66 | totalNumberOfRequests: 0, 67 | averageNumberOfCalls: 0, 68 | numberOfUncachedRequests: 0, 69 | numberOfCachedRequests: 0, 70 | totalTimeSaved: 0, 71 | averageUncachedLatency: 0, 72 | averageCachedLatency: 0, 73 | totalUncachedElapsed: 0, 74 | totalCachedElapsed: 0, 75 | globalAverageCallSpan: 0, 76 | uniqueResolvers: 0, 77 | sizeOfDataRedis: 0, 78 | sizeOfDataLocal: 0, 79 | averageSizeOfDataLocal: 0, 80 | averageCacheThreshold: 0, 81 | }) 82 | ); 83 | 84 | if (configObj.local) { 85 | fs.writeFileSync(`cacheflowSrc/localStorage.json`, '{}'); 86 | globalLocalThreshold = configObj.local.globalThreshold / 1000; 87 | } 88 | 89 | if (configObj.redis) { 90 | client = redis.createClient({ 91 | host: configObj.redis.host, 92 | port: configObj.redis.port, 93 | password: configObj.redis.password, 94 | }); 95 | 96 | client.on('error', (err) => { 97 | throw new Error('ERROR CONNECTING TO REDIS'); 98 | }); 99 | } 100 | 101 | setInterval(() => { 102 | clean(); 103 | }, configObj.local.checkExpire * 1000 || 10000); 104 | }; 105 | 106 | /* 107 | ------------------------------------------------------------- 108 | CACHE FUNCTION: cache(cachedConfig: Object that is passed in by user, info, callback){} 109 | If cacheConfig is incorrect throw error 110 | If cacheConfig.location is local call cachLocal 111 | If cacheConfig.location is redis call cacheRedis 112 | */ 113 | 114 | exports.cache = function (cacheConfig = {}, info, callback) { 115 | const startDate = Date.now(); 116 | if (typeof cacheConfig !== 'object' || Array.isArray(cacheConfig)) 117 | throw new Error('Config object is invalid'); 118 | if (cacheConfig.location === 'local') { 119 | return cacheLocal(cacheConfig, info, callback, startDate); 120 | } 121 | if (cacheConfig.location === 'redis') { 122 | return cacheRedis(cacheConfig, info, callback, startDate); 123 | } 124 | }; 125 | 126 | /* 127 | ------------------------------------------------------------- 128 | LOCAL CACHE FUNCTION: cacheLocal() {} 129 | If resolver was a mutation call mutateLocal 130 | If resolver already in local cache call localFound 131 | If resolver was not in local cache call localNotFound 132 | */ 133 | 134 | async function cacheLocal(cacheConfig, info, callback, startDate) { 135 | const metrics = fsRead('cacheflowSrc/localMetricsStorage.json'); 136 | if (cacheConfig.mutate) { 137 | if (!metrics[cacheConfig.mutate]) { 138 | throw new Error('Data does not exist in local cache'); 139 | } 140 | return mutateLocal(cacheConfig, callback); 141 | } 142 | const parsedData = fsRead('cacheflowSrc/localStorage.json'); 143 | if (parsedData[info.path.key]) { 144 | return localFound(cacheConfig, info, startDate, parsedData); 145 | } else { 146 | return localNotFound(cacheConfig, info, callback, startDate, parsedData); 147 | } 148 | } 149 | 150 | /* 151 | ------------------------------------------------------------- 152 | MUTATE LOCAL FUNCTION: mutateLocal() {} 153 | Update localStorage for the resolver the mutation was called on 154 | Call mutationMetrics with the resolver name 155 | Return data from the callback 156 | */ 157 | 158 | async function mutateLocal(cacheConfig, callback) { 159 | const parsedData = fsRead('cacheflowSrc/localStorage.json'); 160 | const dataBack = await callback(); 161 | parsedData[cacheConfig.mutate] = { 162 | data: dataBack, 163 | expire: Date.now() + cacheConfig.maxAge * 1000, 164 | }; 165 | fsWrite('cacheflowSrc/localStorage.json', parsedData); 166 | mutationMetrics(cacheConfig.mutate, dataBack); 167 | return parsedData[cacheConfig.mutate].data; 168 | } 169 | 170 | /* 171 | ------------------------------------------------------------- 172 | LOCAL FOUND FUNCTION: localFound() {} 173 | Read and parse localStorage.json 174 | Time stamp and log latency 175 | Update expiration date 176 | Call metrics with cachedLatency 177 | Update cache and return cached data 178 | */ 179 | 180 | function localFound(cacheConfig, info, startDate, parsedData) { 181 | const currentTime = Date.now(); 182 | const requestLatencyCached = currentTime - startDate; 183 | parsedData[info.path.key].expire = currentTime + cacheConfig.maxAge * 1000; 184 | metrics({ cachedLatency: requestLatencyCached }, info); 185 | const globalMetrics = fsRead('cacheflowSrc/globalMetrics.json'); 186 | globalMetrics.numberOfCachedRequests++; 187 | globalMetrics.totalCachedElapsed += requestLatencyCached; 188 | globalMetrics.averageCachedLatency = 189 | globalMetrics.totalCachedElapsed / globalMetrics.numberOfCachedRequests; 190 | fsWrite('cacheflowSrc/globalMetrics.json', globalMetrics); 191 | fsWrite('cacheflowSrc/localStorage.json', parsedData); 192 | return parsedData[info.path.key].data; 193 | } 194 | 195 | /* 196 | ------------------------------------------------------------- 197 | LOCAL NOT FOUND FUNCTION: localNotFound() {} 198 | Run callback to generate data 199 | Time stamp 200 | Add new data to parsed object 201 | Log metrics 202 | Cache new data and return new data 203 | 204 | Threshold defaults to global variable globalLocalThreshold unless user has specific 205 | threshold for that resolver on resolver's cacheConfig object 206 | 207 | Smartcache gets called to see if data should be cached or not 208 | */ 209 | 210 | async function localNotFound( 211 | cacheConfig, 212 | info, 213 | callback, 214 | startDate, 215 | parsedData 216 | ) { 217 | const resolverName = info.path.key; 218 | const returnData = await callback(); 219 | const currentTime = Date.now(); 220 | const requestLatencyUncached = currentTime - startDate; 221 | 222 | let localMetrics = fsRead('cacheflowSrc/localMetricsStorage.json'); 223 | let threshold; 224 | let inMetricCheck = false; 225 | 226 | if (!localMetrics[resolverName]) { 227 | inMetricCheck = true; 228 | metrics( 229 | { 230 | uncachedLatency: requestLatencyUncached, 231 | returnData, 232 | storedLocation: 'local', 233 | }, 234 | info 235 | ); 236 | } 237 | 238 | localMetrics = fsRead('cacheflowSrc/localMetricsStorage.json'); 239 | cacheConfig.threshold 240 | ? (threshold = cacheConfig.threshold / 1000) 241 | : (threshold = globalLocalThreshold); 242 | const globalMetrics = fsRead('cacheflowSrc/globalMetrics.json'); 243 | 244 | const allCalls = localMetrics[resolverName].allCalls; 245 | const numberCalls = localMetrics[resolverName].numberOfCalls; 246 | let frequency; 247 | 248 | allCalls.length === 1 249 | ? (frequency = 0) 250 | : (frequency = numberCalls / (allCalls[allCalls.length - 1] - allCalls[0])); 251 | 252 | let smartCacheValue = null; 253 | if (inMetricCheck === false) { 254 | smartCacheValue = smartCache(localMetrics, globalMetrics, resolverName); 255 | } 256 | 257 | if (frequency >= threshold || smartCacheValue) { 258 | parsedData[resolverName] = { 259 | data: returnData, 260 | expire: currentTime + cacheConfig.maxAge * 1000, 261 | }; 262 | globalMetrics.numberOfCachedRequests++; 263 | 264 | fsWrite('cacheflowSrc/globalMetrics.json', globalMetrics); 265 | fsWrite('cacheflowSrc/localStorage.json', parsedData); 266 | return returnData; 267 | } else { 268 | if (inMetricCheck === false) { 269 | metrics( 270 | { 271 | uncachedLatency: requestLatencyUncached, 272 | returnData, 273 | storedLocation: 'local', 274 | }, 275 | info 276 | ); 277 | } 278 | 279 | const globalMetrics = fsRead('cacheflowSrc/globalMetrics.json'); 280 | globalMetrics.numberOfUncachedRequests++; 281 | globalMetrics.totalUncachedElapsed += requestLatencyUncached; 282 | globalMetrics.averageUncachedLatency = 283 | globalMetrics.totalUncachedElapsed / 284 | globalMetrics.numberOfUncachedRequests; 285 | 286 | fsWrite('cacheflowSrc/globalMetrics.json', globalMetrics); 287 | } 288 | return returnData; 289 | } 290 | 291 | /* 292 | ------------------------------------------------------------- 293 | SMART CACHE FUNCTION: smartCache() {} 294 | Uses number of calls, time between calls and size of data to make one comparison variable value 295 | Uses the value variable from above and compares it to average data for all resolvers 296 | If value is greater than the average threshold value for all resolvers return true, else return false 297 | */ 298 | 299 | const smartCache = (metricsData, globalMetrics, resolverName) => { 300 | const defaultThreshold = 1; 301 | 302 | let numberCalls = 303 | (metricsData[resolverName].numberOfCalls - 304 | globalMetrics.averageNumberOfCalls) / 305 | globalMetrics.averageNumberOfCalls; 306 | 307 | let temp; 308 | metricsData[resolverName].averageCallSpan === 'Insufficient Data' 309 | ? (temp = 10000) 310 | : (temp = metricsData[resolverName].averageCallSpan); 311 | let callSpan = metricsData[resolverName].averageCallSpan; 312 | callSpan <= 0 ? (callSpan = 5000) : null; 313 | 314 | let dataSize = 315 | (metricsData[resolverName].dataSize - 316 | globalMetrics.averageSizeOfDataLocal) / 317 | 300; 318 | 319 | const value = numberCalls + (1 / (0.004 * temp)) * 0.92 + dataSize * 0.17; 320 | 321 | if (value > defaultThreshold * 0.97) { 322 | globalMetrics.averageCacheThreshold = 323 | (defaultThreshold + value) / 324 | (globalMetrics.totalNumberOfRequests === 0 325 | ? 1 326 | : globalMetrics.totalNumberOfRequests); 327 | 328 | metricsData[resolverName].cacheThreshold = value; 329 | fsWrite('cacheflowSrc/localMetricsStorage.json', metricsData); 330 | fsWrite('cacheflowSrc/globalMetrics.json', globalMetrics); 331 | return true; 332 | } 333 | fsWrite('cacheflowSrc/globalMetrics.json', globalMetrics); 334 | return false; 335 | }; 336 | 337 | /* 338 | ------------------------------------------------------------- 339 | MUTATE REDIS FUNCTION: redisMutate() {} 340 | New data is generated from callback 341 | Set new mutated data to redis store with new expiration date 342 | Call mutationMetrics to update metrics 343 | Return new mutated data 344 | */ 345 | 346 | async function redisMutate(cacheConfig, callback, startDate) { 347 | const returnData = await callback(); 348 | client.set(cacheConfig.mutate, JSON.stringify(returnData)); 349 | client.expire(cacheConfig.mutate, cacheConfig.maxAge); 350 | mutationMetrics(cacheConfig.mutate, returnData); 351 | return returnData; 352 | } 353 | 354 | /* 355 | ------------------------------------------------------------- 356 | CACHE REDIS FUNCTION: cacheRedis() {} 357 | Must promisify redis client.get function 358 | If user is mutating data, run reisMutate function 359 | Else see if data is in redis store or not, if not cache it and set metrics, else update expiration date and call metrics 360 | */ 361 | 362 | async function cacheRedis(cacheConfig, info, callback, startDate) { 363 | const getAsync = promisify(client.get).bind(client); 364 | const resolverName = info.path.key; 365 | let redisData; 366 | let responseTime; 367 | 368 | if (cacheConfig.mutate) { 369 | return redisMutate(cacheConfig, callback, startDate); 370 | } 371 | 372 | await getAsync(resolverName).then(async (res) => { 373 | if (res === null) { 374 | const returnData = await callback(); 375 | client.set(resolverName, JSON.stringify(returnData)); 376 | client.expire(resolverName, cacheConfig.maxAge); 377 | redisData = returnData; 378 | responseTime = Date.now() - startDate; 379 | metrics( 380 | { 381 | uncachedLatency: responseTime, 382 | storedLocation: 'redis', 383 | returnData, 384 | }, 385 | info 386 | ); 387 | } else { 388 | redisData = JSON.parse(res); 389 | client.expire(resolverName, cacheConfig.maxAge); 390 | responseTime = Date.now() - startDate; 391 | metrics({ cachedLatency: responseTime }, info); 392 | } 393 | }); 394 | 395 | return redisData; 396 | } 397 | 398 | /* 399 | ------------------------------------------------------------- 400 | MUTATE METRICS FUNCTION: mutationMetrics() {} 401 | Update metrics about size of data size of specific resolver after a mutation 402 | Update metrics about size of data size in global cache after a mutation 403 | */ 404 | 405 | function mutationMetrics(mutateName, data) { 406 | const jsonLocal = fsRead('cacheflowSrc/localMetricsStorage.json'); 407 | const jsonGlobal = fsRead('cacheflowSrc/globalMetrics.json'); 408 | 409 | const oldSize = jsonLocal[mutateName].dataSize; 410 | const newSize = sizeOf(data); 411 | 412 | jsonLocal[mutateName].dataSize = newSize; 413 | 414 | jsonGlobal.sizeOfDataLocal += newSize - oldSize; 415 | 416 | fsWrite('cacheflowSrc/localMetricsStorage.json', jsonLocal); 417 | fsWrite('cacheflowSrc/globalMetrics.json', jsonGlobal); 418 | } 419 | 420 | /* 421 | ------------------------------------------------------------- 422 | METRICS FUNCTION: metrics() {} 423 | If resolver in cache call localMetricsUpdate 424 | If resolver not in cache call setLocalMetric 425 | Always call globalMetrics 426 | */ 427 | 428 | async function metrics(resolverData, info) { 429 | let parsedMetrics = fsRead('cacheflowSrc/localMetricsStorage.json'); 430 | 431 | if (parsedMetrics[info.path.key]) { 432 | await localMetricsUpdate(resolverData, info, parsedMetrics); 433 | } else { 434 | await setLocalMetric(resolverData, info, parsedMetrics); 435 | } 436 | await globalMetrics(resolverData, info, parsedMetrics); 437 | } 438 | 439 | /* 440 | ------------------------------------------------------------- 441 | SET LOCAL METRICS FUNCTION: setLocalMetric() {} 442 | Update localMetricsStorage with new resolver 443 | 444 | firstCall: timestamp from first call 445 | allCalls: array of timestamps from calls 446 | numberOfCalls: total number of calls for resolver 447 | averageCallSpan: average time between calls 448 | uncachedCallTime: length of uncached query 449 | cachedCallTime: length of cached query 450 | dataSize: size of data 451 | storedLocation: where the data is stored 452 | */ 453 | 454 | function setLocalMetric(resolverData, info, parsedMetrics) { 455 | globalMetricsParsed = fsRead('cacheflowSrc/globalMetrics.json'); 456 | parsedMetrics[info.path.key] = { 457 | firstCall: Date.now(), 458 | allCalls: [Date.now()], 459 | numberOfCalls: 1, 460 | averageCallSpan: 'Insufficient Data', 461 | uncachedCallTime: resolverData.uncachedLatency, 462 | cachedCallTime: null, 463 | dataSize: sizeOf(resolverData.returnData), 464 | storedLocation: resolverData.storedLocation, 465 | cacheThreshold: null, 466 | }; 467 | fsWrite('cacheflowSrc/localMetricsStorage.json', parsedMetrics); 468 | 469 | resolverData.storedLocation === 'local' 470 | ? (globalMetricsParsed.sizeOfDataLocal += sizeOf(resolverData.returnData)) 471 | : null; 472 | 473 | fsWrite('cacheflowSrc/globalMetrics.json', globalMetricsParsed); 474 | } 475 | 476 | /* 477 | ------------------------------------------------------------- 478 | LOCAL METRICS UPDATE FUNCTION: cacheRedis() {} 479 | Updates allCalls to be array with only last ten calls to resolver 480 | Updates averageCallSpan to be length of time between last call and tenth call ago 481 | Increments numberOfCalls by one 482 | Sets cached call time equal to how long the cached request took 483 | */ 484 | 485 | function localMetricsUpdate(resolverData, info, parsedMetrics) { 486 | const resolverName = info.path.key; 487 | const date = Date.now(); 488 | 489 | let allCalls = parsedMetrics[resolverName].allCalls; 490 | allCalls.push(date); 491 | allCalls.length > 10 ? allCalls.shift() : allCalls; 492 | 493 | if (resolverData.uncachedLatency) { 494 | parsedMetrics[resolverName].uncachedCallTime = resolverData.uncachedLatency; 495 | } 496 | 497 | parsedMetrics[resolverName].averageCallSpan = 498 | (date - allCalls[0]) / allCalls.length; 499 | parsedMetrics[resolverName].numberOfCalls += 1; 500 | parsedMetrics[resolverName].cachedCallTime = resolverData.cachedLatency; 501 | 502 | fsWrite('cacheflowSrc/localMetricsStorage.json', parsedMetrics); 503 | } 504 | 505 | /* 506 | ------------------------------------------------------------- 507 | GLOBAL METRICS FUNCTION: globalMetrics() {} 508 | Increments totalNumberOfRequests by one 509 | Increments totalTimeSaved by the difference between the cached and uncached requests for that resolver 510 | Updates amount of data saved locally 511 | */ 512 | 513 | function globalMetrics(resolverData, info, parsedMetrics) { 514 | const resolverName = info.path.key; 515 | const numOfResolvers = Object.keys(parsedMetrics).length; 516 | let globalMetricsParsed = fsRead('cacheflowSrc/globalMetrics.json'); 517 | 518 | globalMetricsParsed.totalNumberOfRequests++; 519 | 520 | globalMetricsParsed.averageNumberOfCalls = 521 | globalMetricsParsed.totalNumberOfRequests / numOfResolvers; 522 | 523 | globalMetricsParsed.totalTimeSaved += 524 | parsedMetrics[resolverName].uncachedCallTime - 525 | parsedMetrics[resolverName].cachedCallTime; 526 | 527 | globalMetricsParsed.uniqueResolvers = numOfResolvers; 528 | 529 | globalMetricsParsed.averageSizeOfDataLocal = 530 | globalMetricsParsed.sizeOfDataLocal / numOfResolvers; 531 | 532 | let globalAvgCallSpan = 0; 533 | for (const item in parsedMetrics) { 534 | globalAvgCallSpan += parsedMetrics[item].averageCallSpan; 535 | } 536 | globalMetricsParsed.globalAverageCallSpan = 537 | globalAvgCallSpan / globalMetricsParsed.uniqueResolvers; 538 | 539 | fsWrite('cacheflowSrc/globalMetrics.json', globalMetricsParsed); 540 | } 541 | 542 | /* 543 | ------------------------------------------------------------- 544 | CLEAN STORAGE FUNCTION: clean() {} 545 | Checks if any data stored locally is set to expire, deletes it from localStorage if its expire property is greater than Date.now() 546 | Updates local metrics for that resolver and global metrics 547 | */ 548 | 549 | function clean() { 550 | const dateNow = Date.now(); 551 | 552 | let parsedData = fsRead('cacheflowSrc/localStorage.json'); 553 | let parsedGlobalData = fsRead('cacheflowSrc/globalMetrics.json'); 554 | let parsedLocalData = fsRead('cacheflowSrc/localMetricsStorage.json'); 555 | 556 | let sizeOfDeletedDataLocal = 0; 557 | 558 | for (let resolver in parsedData) { 559 | if (dateNow > parsedData[resolver].expire) { 560 | sizeOfDeletedDataLocal += parsedLocalData[resolver].dataSize; 561 | parsedLocalData[resolver].dataSize = 0; 562 | delete parsedData[resolver]; 563 | } 564 | } 565 | 566 | if (client) { 567 | client.info((req, res) => { 568 | res.split('\n').map((line) => { 569 | if (line.match(/used_memory:/)) { 570 | parsedGlobalData.sizeOfDataRedis = parseInt(line.split(':')[1]); 571 | parsedGlobalData.sizeOfDataLocal -= sizeOfDeletedDataLocal; 572 | 573 | fsWrite('cacheflowSrc/globalMetrics.json', parsedGlobalData); 574 | } 575 | }); 576 | }); 577 | } 578 | 579 | fsWrite('cacheflowSrc/localStorage.json', parsedData); 580 | fsWrite('cacheflowSrc/localMetricsStorage.json', parsedLocalData); 581 | } 582 | 583 | /* 584 | ------------------------------------------------------------- 585 | FS FUNCTIONS: 586 | fsRead(){} 587 | fsWrite(){} 588 | */ 589 | 590 | function fsRead(fileName) { 591 | const data = fs.readFileSync(`${fileName}`, 'utf-8'); 592 | const json = JSON.parse(data); 593 | return json; 594 | } 595 | 596 | function fsWrite(fileName, data) { 597 | fs.writeFileSync(`${fileName}`, JSON.stringify(data), (err) => { 598 | if (err) throw new Error(err); 599 | }); 600 | } 601 | 602 | /* 603 | ------------------------------------------------------------- 604 | DATA SIZE FUNCTION: sizeOf() {} 605 | Returns an estimated size of input data in bytes 606 | */ 607 | 608 | const typeSizes = { 609 | undefined: () => 0, 610 | boolean: () => 4, 611 | number: () => 8, 612 | string: (item) => 2 * item.length, 613 | object: (item) => 614 | !item 615 | ? 0 616 | : Object.keys(item).reduce( 617 | (total, key) => sizeOf(key) + sizeOf(item[key]) + total, 618 | 0 619 | ), 620 | }; 621 | 622 | const sizeOf = (value) => typeSizes[typeof value](value); 623 | -------------------------------------------------------------------------------- /metricsTerminal.js: -------------------------------------------------------------------------------- 1 | const readline = require('readline'); 2 | const fs = require('fs'); 3 | 4 | const rl = readline.createInterface({ 5 | input: process.stdin, 6 | output: process.stdout, 7 | }); 8 | 9 | function terminalPrompt() { 10 | rl.question( 11 | '\nEnter your desired metrics:\n --> Global Metrics\n --> Name of Resolver\n\n=======================================================================================================\n\n', 12 | (answer) => { 13 | if ( 14 | answer.toLowerCase() === 'global metrics' || 15 | answer.toLowerCase() === 'global' 16 | ) { 17 | const globalMetricsData = fs.readFileSync( 18 | './globalMetrics.json', 19 | 'utf-8' 20 | ); 21 | const jsonGMD = JSON.parse(globalMetricsData); 22 | 23 | console.log( 24 | '\n=======================================================================================================' 25 | ); 26 | console.log( 27 | '\nTotal Number of Query Requests: ' + 28 | jsonGMD.totalNumberOfRequests + 29 | ' requests' 30 | ); 31 | console.log( 32 | '\nAverage Number of Requests Per Resolver: ' + 33 | jsonGMD.averageNumberOfCalls + 34 | ' requests' 35 | ); 36 | console.log( 37 | '\nNumber of Uncached Requests: ' + 38 | jsonGMD.numberOfUncachedRequests + 39 | ' requests' 40 | ); 41 | console.log( 42 | '\nNumber of Cached Requests: ' + 43 | jsonGMD.numberOfCachedRequests + 44 | ' requests' 45 | ); 46 | console.log( 47 | '\nAverage Cached Latency: ' + jsonGMD.averageUncachedLatency + ' ms' 48 | ); 49 | console.log( 50 | '\nAverage Cached Latency: ' + jsonGMD.averageCachedLatency + ' ms' 51 | ); 52 | console.log('Total Time Saved: ' + jsonGMD.totalTimeSaved + ' ms'); 53 | console.log( 54 | '\nUnique Resolvers: ' + jsonGMD.uniqueResolvers + ' requests' 55 | ); 56 | console.log( 57 | 'Total Amount of Data Saved to Redis: ' + 58 | jsonGMD.sizeOfDataRedis + 59 | ' bytes' 60 | ); 61 | console.log( 62 | 'Total Amount of Data Saved Locally: ' + 63 | jsonGMD.sizeOfDataLocal + 64 | ' bytes' 65 | ); 66 | console.log( 67 | '\n=======================================================================================================' 68 | ); 69 | } else { 70 | const localMetricsData = fs.readFileSync( 71 | './localMetricsStorage.json', 72 | 'utf-8' 73 | ); 74 | const jsonLMD = JSON.parse(localMetricsData); 75 | 76 | if (jsonLMD[answer]) { 77 | console.log( 78 | '\n=======================================================================================================', 79 | `\n\nData for "${answer}":` 80 | ); 81 | console.log( 82 | '\nFirst Time Called: ', 83 | new Date(jsonLMD[answer].firstCall).toString() 84 | ); 85 | console.log( 86 | 'Last Time Called: ', 87 | new Date( 88 | jsonLMD[answer].allCalls[jsonLMD[answer].allCalls.length - 1] 89 | ).toString() 90 | ); 91 | console.log('Number of Calls: ' + jsonLMD[answer].numberOfCalls); 92 | console.log( 93 | 'Average Time Between calls: ', 94 | jsonLMD[answer].averageCallSpan === 'Insufficient Data' 95 | ? 'Insufficient Data' 96 | : jsonLMD[answer].averageCallSpan + ' ms' 97 | ); 98 | console.log( 99 | 'Uncached Latency: ' + jsonLMD[answer].uncachedCallTime + ' ms' 100 | ); 101 | console.log( 102 | 'Cached Latency: ' + jsonLMD[answer].cachedCallTime + ' ms' 103 | ); 104 | console.log( 105 | 'Time Saved by Caching: ' + 106 | (jsonLMD[answer].uncachedCallTime - 107 | jsonLMD[answer].cachedCallTime) + 108 | ' ms' 109 | ); 110 | console.log( 111 | 'Size of Cached Query: ' + jsonLMD[answer].dataSize + ' bytes' 112 | ); 113 | console.log( 114 | 'Location of Cached Data:', 115 | jsonLMD[answer].storedLocation 116 | ); 117 | console.log( 118 | '\n=======================================================================================================' 119 | ); 120 | } else { 121 | console.log('\nNo Cached Data Found for', `"${answer}"`); 122 | } 123 | } 124 | terminalPrompt(); 125 | } 126 | ); 127 | } 128 | 129 | terminalPrompt(); 130 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cacheflowql", 3 | "version": "1.1.3", 4 | "description": "An advanced solution for all your graphQL queries", 5 | "main": "cacheflow.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/oslabs-beta/cacheflow.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/oslabs-beta/cacheflow/issues" 17 | }, 18 | "homepage": "https://github.com/oslabs-beta/cacheflow#readme", 19 | "dependencies": { 20 | "redis": "^3.1.2" 21 | }, 22 | "devDependencies": { 23 | "jest": "^27.0.6" 24 | } 25 | } 26 | --------------------------------------------------------------------------------