Skip to content

Commit

Permalink
Merge pull request #419 from magieno/try-to-improve-speed-using-contr…
Browse files Browse the repository at this point in the history
…oller-caching

Try to improve speed using controller caching
  • Loading branch information
mathieugh authored Nov 1, 2022
2 parents 268df0a + 1b69621 commit 977cb93
Show file tree
Hide file tree
Showing 21 changed files with 637 additions and 510 deletions.
120 changes: 38 additions & 82 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"@types/jest": "^27.4.1",
"@types/lodash": "^4.14.168",
"@types/node": "^14.14.31",
"@types/url-parse": "^1.4.3",
"@typescript-eslint/eslint-plugin": "^5.16.0",
"@typescript-eslint/parser": "^5.16.0",
"eslint": "^8.11.0",
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/common.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from "./errors/errors";
export * from "./models/models";
export * from "./interfaces/interfaces";
export * from "./types/types";
export * from "./utils/utils";
export * from "./common.module.keyname";

export const CommonModule: ModuleInterface = {
Expand Down
66 changes: 66 additions & 0 deletions packages/common/src/utils/request.util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {Request} from "../models/request";
import {RequestUtil} from "./request.util";
import {HttpMethod} from "../enums/http-method.enum";

describe("Request Util", () => {
it("should hash the same request twice", () => {
const rawBody = {};

const request: Request = new Request(HttpMethod.Get, "http://www.subdomain.ima-tech.ca/api/1.0/dogs/caniche-royal?query=searchTerm&sort=ASC#anchorLink");
request.rawBody = rawBody;
request.setHeaders({
"header1": "value1",
"header2": "value2",
"header3": "value3",
});

const request2: Request = new Request(HttpMethod.Get, "http://www.subdomain.ima-tech.ca/api/1.0/dogs/caniche-royal?sort=ASC&query=searchTerm#anchorLink");;
request2.rawBody = rawBody;
request2.setHeaders({
"header3": "value3",
"header2": "value2",
"header1": "value1",
});

const request1Hash = RequestUtil.hash(request);
const request2Hash = RequestUtil.hash(request2);

expect(request1Hash).toBe(request2Hash);
})

it("should not hash different requests to the same value", () => {
const rawBody = {};

const request: Request = new Request(HttpMethod.Get, "http://www.subdomain.ima-tech.ca/api/1.0/dogs/caniche-royal?query=searchTerm&sort=ASC#anchorLink");
request.rawBody = rawBody;
request.setHeaders({
"header1": "value1",
"header2": "value2",
"header3": "value3",
});

const request2: Request = new Request(HttpMethod.Get, "http://www.subdomain.ima-tech.ca/api/1.0/dogs/caniche-royal?sort=ASC&query=searchTermXXXXX#anchorLink");;
request2.rawBody = rawBody;
request2.setHeaders({
"header3": "value3",
"header2": "value2",
"header1": "value1",
});

const request3: Request = new Request(HttpMethod.Get, "http://www.subdomain.ima-tech.ca/api/1.0/dogs/caniche-royal?sort=ASC&query=searchTerm");
request2.rawBody = rawBody;
request2.setHeaders({
"header3": "value3",
"header2": "value2",
"header1": "value1",
});

const request1Hash = RequestUtil.hash(request);
const request2Hash = RequestUtil.hash(request2);
const request3Hash = RequestUtil.hash(request3);

expect(request1Hash).not.toBe(request2Hash);
expect(request1Hash).not.toBe(request3Hash);
expect(request2Hash).not.toBe(request3Hash);
})
})
38 changes: 38 additions & 0 deletions packages/common/src/utils/request.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { createHash } from 'crypto';
import {Request} from "../models/request";
import { URL } from 'url';

function sort(obj: any) {
const ret: any = {};

Object.keys(obj).sort().forEach(function (key) {
ret[key] = obj[key];
});

return ret;
}

export class RequestUtil {
static hash(request: Request): string | null {
const hash = createHash("md5");

const parsedUrl = new URL(request.url);

parsedUrl.searchParams.sort();

hash.update(parsedUrl.pathname);
hash.update(request.httpMethod);
hash.update(parsedUrl.searchParams.toString());
hash.update(parsedUrl.hash);
hash.update(JSON.stringify(sort(request.headers)));

try {
hash.write(JSON.stringify(request.body));
} catch (e) {
return null;
}


return hash.digest("hex");
}
}
1 change: 1 addition & 0 deletions packages/common/src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./request.util";
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"@pristine-ts/logging": "file:../logging",
"@pristine-ts/security": "file:../security",
"@pristine-ts/telemetry": "file:../telemetry",
"uuid": "^8.3.2"
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/uuid": "^8.3.3"
Expand Down
3 changes: 1 addition & 2 deletions packages/express/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@
"@pristine-ts/networking": "file:../networking",
"@types/express": "^4.17.9",
"express": "^4.17.1",
"reflect-metadata": "^0.1.13",
"url-parse": "^1.4.7"
"reflect-metadata": "^0.1.13"
},
"jest": {
"transform": {
Expand Down
3 changes: 1 addition & 2 deletions packages/http/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
"dependencies": {
"@pristine-ts/common": "file:../common",
"@pristine-ts/logging": "file:../logging",
"lodash": "^4.17.21",
"url-parse": "^1.4.7"
"lodash": "^4.17.21"
},
"author": "",
"license": "ISC",
Expand Down
6 changes: 3 additions & 3 deletions packages/http/src/clients/http.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {inject, injectable, injectAll} from "tsyringe";
import {HttpClientInterface} from "../interfaces/http-client.interface";
import {HttpRequestInterface} from "../interfaces/http-request.interface";
import {HttpResponseInterface} from "../interfaces/http-response.interface";
import Url from 'url-parse';
import { URL } from 'url';
import {ResponseTypeEnum} from "../enums/response-type.enum";
import {HttpRequestOptions} from "../options/http-request.options.";
import {assign} from "lodash";
Expand Down Expand Up @@ -185,8 +185,8 @@ export class HttpClient implements HttpClientInterface {
const updatedRequest = request;

// Updated the URL by using the 'location' header returned by the response.
const url = new Url(request.url, true);
url.set("pathname", response.headers.location);
const url = new URL(request.url);
url.pathname = response.headers.location;

updatedRequest.url = url.toString()

Expand Down
4 changes: 2 additions & 2 deletions packages/http/src/errors/http-client-request.error.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {LoggableError} from "@pristine-ts/common";
import {HttpRequestInterface} from "../interfaces/http-request.interface";
import Url from "url-parse";
import { URL } from 'url';

/**
* This Error represents an error when making an http request using the http client
*/
export class HttpClientRequestError extends LoggableError {
public constructor(readonly message: string, readonly request: HttpRequestInterface, readonly url: Url) {
public constructor(readonly message: string, readonly request: HttpRequestInterface, readonly url: URL) {
super(message, {
request,
url,
Expand Down
6 changes: 3 additions & 3 deletions packages/http/src/wrappers/http.wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {HttpResponseInterface} from "../interfaces/http-response.interface";
import * as http from "http";
import {IncomingMessage, request as httpRequest, RequestOptions} from "http";
import {request as httpsRequest} from "https";
import Url from 'url-parse';
import { URL } from 'url';
import {tag} from "@pristine-ts/common";
import {HttpClientRequestError} from "../errors/http-client-request.error";
import {HttpWrapperInterface} from "../interfaces/http-wrapper.interface";
Expand All @@ -24,10 +24,10 @@ export class HttpWrapper implements HttpWrapperInterface {
executeRequest(request: HttpRequestInterface): Promise<HttpResponseInterface> {
return new Promise((resolve, reject) => {
// Define the options required by the http and https modules.
const url = new Url(request.url, true);
const url = new URL(request.url);
const options: RequestOptions = {
host: url.hostname,
path: url.pathname + ((url.query === {}) ? "" : "?" + Object.keys(url.query).map(key => key + "=" + querystring.escape(url.query[key] ?? "")).join("&")),
path: url.pathname + url.search,
method: request.httpMethod,
headers: request.headers,
port: url.port,
Expand Down
3 changes: 1 addition & 2 deletions packages/networking/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@
"@pristine-ts/core": "file:../core",
"@pristine-ts/security": "file:../security",
"@pristine-ts/telemetry": "file:../telemetry",
"lodash": "^4.17.21",
"url-parse": "^1.4.7"
"lodash": "^4.17.21"
},
"jest": {
"transform": {
Expand Down
2 changes: 2 additions & 0 deletions packages/networking/src/cache/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./cached.router-route";
export * from "./router.cache";
39 changes: 39 additions & 0 deletions packages/networking/src/cache/cached.router-route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {MethodRouterNode} from "../nodes/method-router.node";
import {Request, RequestUtil} from "@pristine-ts/common";

export class CachedRouterRoute {
private cachedControllerMethodArguments: { [requestHash: string]: any[] } = {};
public routeParameters?: { [key: string]: string };

constructor(public readonly methodNode: MethodRouterNode,
) {
}

private hashRequest(request: Request): string | null {
return RequestUtil.hash(request);
}

getCachedControllerMethodArguments(request: Request): any[] | undefined {
// Hashed the request
const hash = this.hashRequest(request);

if(hash === null) {
return;
}

// Return the arguments if the hashed request exists
return this.cachedControllerMethodArguments[hash];
}

cacheControllerMethodArguments(request: Request, methodArguments: any[]): void {
// Hashed the request
const hash = this.hashRequest(request);

if(hash === null) {
return;
}

// Save the method arguments
this.cachedControllerMethodArguments[hash] = methodArguments;
}
}
61 changes: 61 additions & 0 deletions packages/networking/src/cache/router.cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {Request, tag} from "@pristine-ts/common";
import {singleton, injectable, inject} from "tsyringe";
import {CachedRouterRoute} from "./cached.router-route";
import {MethodRouterNode} from "../nodes/method-router.node";
import {NetworkingModuleKeyname} from "../networking.module.keyname";

@injectable()
export class RouterCache {
public routes: {[methodAndPath: string] : CachedRouterRoute} = {};

public constructor(@inject(`%${NetworkingModuleKeyname}.routerCache.isActive%`) private readonly isActive: boolean) {
}

private cleanIfNeeded() {
// todo: implement a proper LRU cache and also, remove the two layers of cache and flatten it to one.
}

public get(keyname: string): CachedRouterRoute | undefined {
if(this.isActive === false) {
return undefined;
}

return this.routes[keyname];
}

public set(keyname: string, methodNode: MethodRouterNode): CachedRouterRoute {
const cachedRouterRoute = new CachedRouterRoute(methodNode);

if(this.isActive === false) {
return cachedRouterRoute;
}

// todo Calculate the size and add it to the current total

// Whenever we add a new element to the cache, we have to check if the cache needs to be cleaned
this.cleanIfNeeded();

this.routes[keyname] = cachedRouterRoute;

return cachedRouterRoute;
}

public getCachedControllerMethodArguments(keyname: string, request: Request): any[] | undefined {
if(this.isActive === false) {
return undefined;
}

return this.routes[keyname]?.getCachedControllerMethodArguments(request);
}

public setControllerMethodArguments(keyname: string, request: Request, methodArguments: any[]) {
if(this.isActive === false) {
return;
}

this.routes[keyname]?.cacheControllerMethodArguments(request, methodArguments);

return;
}

}
13 changes: 13 additions & 0 deletions packages/networking/src/networking.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {SecurityModule} from "@pristine-ts/security";
import {TelemetryModule} from "@pristine-ts/telemetry";
import {BooleanResolver, EnvironmentVariableResolver} from "@pristine-ts/configuration";

export * from "./cache/cache";
export * from "./decorators/decorators";
export * from "./errors/errors";
export * from "./handlers/handlers";
Expand Down Expand Up @@ -58,6 +59,18 @@ export const NetworkingModule: ModuleInterface = {
defaultResolvers: [
new EnvironmentVariableResolver("PRISTINE_NETWORKING_DEFAULT_CONTENT_TYPE_RESPONSE_HEADER"),
],
},

/**
* Activates or deactivates whether the Router Cache is on or off.
*/
{
parameterName: NetworkingModuleKeyname + ".routerCache.isActive",
isRequired: false,
defaultValue: false,
defaultResolvers: [
new BooleanResolver(new EnvironmentVariableResolver("PRISTINE_NETWORKING_ROUTER_CACHE_IS_ACTIVE")),
],
}
],
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {ControllerMethodParameterDecoratorResolverInterface} from "../interfaces
import {Request} from "@pristine-ts/common";
import {IdentityInterface, moduleScoped, ServiceDefinitionTagEnum, tag} from "@pristine-ts/common";
import {NetworkingModuleKeyname} from "../networking.module.keyname";
import Url from 'url-parse';
import { URL } from 'url';
import {ParameterDecoratorInterface} from "../interfaces/parameter-decorator.interface";
import {QueryParameterDecoratorInterface} from "../interfaces/query-parameter-decorator.interface";

Expand All @@ -30,9 +30,9 @@ export class QueryParameterDecoratorResolver implements ControllerMethodParamete
request: Request,
routeParameters: { [p: string]: string },
identity?: IdentityInterface): Promise<any> {
const url = new Url(request.url, true);
const url = new URL(request.url);

return Promise.resolve(url.query[methodArgument.queryParameterName] ?? null);
return Promise.resolve(url.searchParams.get(methodArgument.queryParameterName) ?? null);
}

/**
Expand Down
Loading

0 comments on commit 977cb93

Please sign in to comment.