Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix the floating setTimeout in introspection client #655

Merged
merged 52 commits into from
Feb 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
fb72f93
Fix the floating setTimeout
calebmshafer Jan 25, 2021
ddee960
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 25, 2021
57c71cb
replace usage of setTimeout in tests to use sinon.clock instead
calebmshafer Jan 26, 2021
a18a338
Merge branch 'fix-floating-setTimeout' of github.com:imodeljs/imodelj…
calebmshafer Jan 26, 2021
90f38dd
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 26, 2021
e0587de
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 26, 2021
b0ba3d7
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 26, 2021
73a99bf
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 26, 2021
823f278
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 26, 2021
26d16e0
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 26, 2021
39fb1ee
fix lint errors
calebmshafer Jan 26, 2021
297d66e
extract-api
calebmshafer Jan 26, 2021
624a9f7
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 26, 2021
6e6b306
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 26, 2021
09e783c
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 26, 2021
f201b63
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 26, 2021
af48678
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 27, 2021
db61498
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 27, 2021
88e9f56
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 27, 2021
4032c80
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 27, 2021
1c57e28
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 27, 2021
978f8bc
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 27, 2021
6bb6e40
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 27, 2021
518b917
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 28, 2021
ea0629d
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 28, 2021
1849114
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 28, 2021
b82142b
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 29, 2021
da0ea1f
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 29, 2021
f5de78d
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 29, 2021
0cfccf7
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 29, 2021
0662e9f
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 29, 2021
ddedadd
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 30, 2021
33fc6b0
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 30, 2021
6341ba7
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 31, 2021
f93b5a6
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Jan 31, 2021
6cfe22c
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Feb 1, 2021
a5ada62
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Feb 1, 2021
d8ee3c1
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Feb 1, 2021
85c4132
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Feb 1, 2021
64e21f9
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Feb 1, 2021
1d31800
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Feb 1, 2021
ed52eee
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Feb 1, 2021
040d7da
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Feb 1, 2021
a794e7b
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Feb 2, 2021
dc444f8
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Feb 2, 2021
f23559c
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Feb 2, 2021
13c1a54
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Feb 2, 2021
d4aab96
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Feb 3, 2021
184bc01
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Feb 3, 2021
ed2ef2a
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Feb 3, 2021
a0a8443
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Feb 3, 2021
babfae5
Merge branch 'master' into fix-floating-setTimeout
mergify[bot] Feb 3, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,23 @@
"${workspaceFolder}/{core,clients}/*/lib/**/*.js"
]
},
{
"name": "Backend iTwin Client Tests (Offline)",
"presentation": {
"group": "3_ClientsTests",
"order": 3
},
"cwd": "${workspaceFolder}/core/backend-itwin-client",
"type": "pwa-node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": [
"test"
],
"outFiles": [
"${workspaceFolder}/{core,clients}/*/lib/**/*.js"
]
},
// UI TESTS
{
"name": "[UI] Tests: Abstract",
Expand Down
26 changes: 13 additions & 13 deletions clients/telemetry/src/TelemetryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,34 +61,34 @@ export interface TelemetryClient {

/** @alpha */
export class TelemetryManager {
protected readonly _subClients: Set<TelemetryClient>;
protected readonly _clients: Set<TelemetryClient>;

constructor(...subClients: TelemetryClient[]) {
this._subClients = new Set<TelemetryClient>(subClients);
constructor(...clients: TelemetryClient[]) {
this._clients = new Set<TelemetryClient>(clients);
}

/**
*
* This function should not throw errors
* @param requestContext
* @param telemetryEvent
*/
public async postTelemetry(requestContext: AuthorizedClientRequestContext, telemetryEvent: TelemetryEvent): Promise<void> {
const subClientPromises = Array.from(this._subClients).map(async (subClient) => {
const postPerClient = async (subClient: TelemetryClient) => {
try {
await subClient.postTelemetry(requestContext, telemetryEvent);
} catch (err) {
Logger.logError(TelemetryClientLoggerCategory.Telemetry, `Failed to post telemetry via subclient`, () => err);
}
});
};

const subClientPromises = [];
for (const subClient of this._clients) {
subClientPromises.push(postPerClient(subClient));
}

await Promise.all(subClientPromises);
}

public addClient(client: TelemetryClient): void {
this._subClients.add(client);
this._clients.add(client);
}

public hasClient(client: TelemetryClient): boolean {
return this._subClients.has(client);
return this._clients.has(client);
}
}
7 changes: 4 additions & 3 deletions common/api/telemetry-client.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,15 @@ export class TelemetryEvent {

// @alpha (undocumented)
export class TelemetryManager {
constructor(...subClients: TelemetryClient[]);
constructor(...clients: TelemetryClient[]);
// (undocumented)
addClient(client: TelemetryClient): void;
// (undocumented)
protected readonly _clients: Set<TelemetryClient>;
// (undocumented)
hasClient(client: TelemetryClient): boolean;
postTelemetry(requestContext: AuthorizedClientRequestContext, telemetryEvent: TelemetryEvent): Promise<void>;
// (undocumented)
protected readonly _subClients: Set<TelemetryClient>;
postTelemetry(requestContext: AuthorizedClientRequestContext, telemetryEvent: TelemetryEvent): Promise<void>;
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@bentley/backend-itwin-client",
"comment": "",
"type": "none"
}
],
"packageName": "@bentley/backend-itwin-client",
"email": "31107829+calebmshafer@users.noreply.github.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@bentley/telemetry-client",
"comment": "",
"type": "none"
}
],
"packageName": "@bentley/telemetry-client",
"email": "31107829+calebmshafer@users.noreply.github.com"
}
6 changes: 4 additions & 2 deletions core/backend-itwin-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"extract-api": "betools extract-api --entry=backend-itwin-client",
"lint": "eslint -f visualstudio \"./src/**/*.ts\" 1>&2",
"pretest": "npm run copy:test-assets",
"test": "",
"test": "betools test --grep=\"#integration\" --invert",
"test:integration": "npm run pretest && betools test --grep=\"#integration\""
},
"repository": {
Expand Down Expand Up @@ -73,13 +73,15 @@
"@types/nock": "^9.1.2",
"@types/node": "10.14.1",
"@types/semver": "^5.5.0",
"@types/sinon": "^9.0.0",
"chai": "^4.1.2",
"cpx": "^1.5.0",
"eslint": "^6.8.0",
"mocha": "^5.2.0",
"nock": "^9.2.3",
"nyc": "^14.0.0",
"rimraf": "^3.0.2",
"sinon": "^9.0.2",
"source-map-support": "^0.5.6",
"ts-node": "^7.0.1",
"typescript": "~3.7.4"
Expand All @@ -105,4 +107,4 @@
],
"extends": "plugin:@bentley/imodeljs-recommended"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,52 +8,60 @@

import { IntrospectionResponse } from "./IntrospectionResponse";

/**
/** The IntrospectionResponseCache is a simple dictionary of [[IntrospectionResponse]] that are currently in use.
* The entries will removed automatically if their tokens expire.
* @alpha
* The class UserTokenCache is a simple dictionary of tokens that are currently in use. The entries
* will removed automatically if their tokens expire.
*/
export abstract class IntrospectionResponseCache {
/**
* Adds the given token to the cache. The token will removed automatically from the cache if
* it's about to expire.
* @param key Key of the token entry.
* @param token UserToken that is associated with the key.
/** Adds the given response to the cache. The response will be added if it has not yet expired.
* @param key A unique string to identify the response within the cache.
* @param response A response associated with the key.
*/
public async add(key: string, response: IntrospectionResponse): Promise<void> {
if (!response.exp) // do not cache any response without a known expiration time
return;

const currentTimeInSeconds = new Date().getTime() / 1000; // UTC time in seconds since 1970-01-01
const secondsUntilExpiration = response.exp - currentTimeInSeconds;
if (secondsUntilExpiration > 0) {
setTimeout(async () => this.remove(key), secondsUntilExpiration * 1000);
if (secondsUntilExpiration > 0)
await this.storeResponse(key, response);
}
}

protected abstract async storeResponse(key: string, response: IntrospectionResponse): Promise<void>;
protected abstract storeResponse(key: string, response: IntrospectionResponse): Promise<void>;

/**
* Gets the User token for the given key.
* @param key Key of the token entry.
* @returns The Token if it can be found or undefined if not.
/** Gets the [[IntrospectionResponse]] for the given key.
*
* Note: Removes the response if it has already expired.
*
* @param key Key of the token entry.
* @returns If the key exists and has not yet expired, the IntrospectionResponse associated with the provided key. Otherwise, undefined.
*/
public async get(key: string): Promise<IntrospectionResponse | undefined> {
return this.getResponse(key);
const response = await this.getResponse(key);
if (undefined === response)
return undefined;

const currentTimeInSeconds = new Date().getTime() / 1000; // UTC time in seconds since 1970-01-01
const secondsUntilExpiration = response.exp! - currentTimeInSeconds;
if (secondsUntilExpiration <= 0) {
await this.remove(key);
return undefined;
}

return response;
}

protected abstract async getResponse(key: string): Promise<IntrospectionResponse | undefined>;
protected abstract getResponse(key: string): Promise<IntrospectionResponse | undefined>;

/**
* Removes the token with the given key from the cache.
* @param key Key of the token entry.
* Removes the response associated with the given key from the cache.
* @param key Key of the response entry.
*/
private async remove(key: string): Promise<void> {
await this.deleteResponse(key);
}

protected abstract async deleteResponse(key: string): Promise<void>;
protected abstract deleteResponse(key: string): Promise<void>;
}

/** @alpha */
Expand Down
83 changes: 83 additions & 0 deletions core/backend-itwin-client/src/test/Introspection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/

import { assert } from "chai";
import * as sinon from "sinon";
import { MemoryIntrospectionResponseCache } from "../oidc/introspection/IntrospectionResponseCache";
import { IntrospectionResponse } from "../oidc/introspection/IntrospectionResponse";

describe("MemoryIntrospectionResponseCache", async () => {
it("adds the token to the cache with a valid expiration", async () => {
const testRes: IntrospectionResponse = {
active: true,
client_id: "test", // eslint-disable-line @typescript-eslint/naming-convention
scope: "test",
exp: Date.now() + 10000, // make the timeout long enough it won't be removed.
};

const newCache = new MemoryIntrospectionResponseCache();
await newCache.add("test", testRes);

const res = await newCache.get("test");
assert.isDefined(res);
assert.equal(res?.client_id, "test");
});

it("does not add the token to the cache when the token is already expired", async () => {
const testRes: IntrospectionResponse = {
active: true,
client_id: "test", // eslint-disable-line @typescript-eslint/naming-convention
scope: "test",
exp: new Date().getTime() / 1000 - 10000, // make the timeout prior to the time right now.
};

const newCache = new MemoryIntrospectionResponseCache();
await newCache.add("test", testRes);

const res = await newCache.get("test");
assert.isUndefined(res);
});

it("does not add to the cache if missing an expiration in response object", async () => {
const testRes: IntrospectionResponse = {
active: true,
client_id: "test", // eslint-disable-line @typescript-eslint/naming-convention
scope: "test",
};

const newCache = new MemoryIntrospectionResponseCache();
await newCache.add("test", testRes);

const res = await newCache.get("test");
assert.isUndefined(res);
});

it("adds the response to the cache and removes it after a timeout", async () => {
const clock = sinon.useFakeTimers();

const testRes: IntrospectionResponse = {
active: true,
client_id: "test", // eslint-disable-line @typescript-eslint/naming-convention
scope: "test",
exp: (new Date().getTime() + 10) / 1000,
};

const newCache = new MemoryIntrospectionResponseCache();
await newCache.add("test", testRes);

let res = await newCache.get("test");
assert.isDefined(res);
assert.equal(res?.client_id, "test");

// set clock to go past timeout
clock.tick(100);

// the key should be removed
res = await newCache.get("test");
assert.isUndefined(res);

clock.restore();
});
});