├── 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 |
126 |
127 |
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 |
--------------------------------------------------------------------------------