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

bug: fix metadata-token-service.ts #135

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

Zork33
Copy link
Contributor

@Zork33 Zork33 commented Jul 25, 2023

close #130

Copy link
Contributor

@nikolaymatrosov nikolaymatrosov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не уверен, что схема с lastFetch будет работать.

@Zork33 Zork33 force-pushed the fix-metadata-token-service branch from f9df471 to 7256bcf Compare August 1, 2023 06:44
@Zork33
Copy link
Contributor Author

Zork33 commented Aug 1, 2023

Talked to Iam services, found the tokenization rules. Now the code is based on the received expires_in and the rule that the returned token should expire not earlier than in 15 min.

}

return this.token;
return this.token as string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the TypeScript code review, it is advised to avoid using the as keyword wherever possible. Relying on as can obscure potential type-related problems in the code, so it's better to find alternative approaches that ensure type safety and clarity throughout the codebase.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case "as" is necessary for an optional field to become mandatory. Before this line there is a check that the value is defined. Probably need some kind of hint for ts review.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.token! is more idiomatic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

превел на ! там где это подходит

// deduplicate initialize requests in any async case
if (!this.currentInitialize) {
this.currentInitialize = this._initialize().finally(() => {
delete this.currentInitialize;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, prefer assigning null to a class property rather than using the delete operator because it is more straightforward, safer, and more in line with good TypeScript practices.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good rule in Java. In TS, if a variable is described as "private token?: string;" delete is a switch to the initial state. Other variants require additional testing

try {
// eslint-disable-next-line no-await-in-loop
this.token = await this.fetchToken();
break;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a delay when implementing retry improves code resilience by mitigating transient failures, preventing server overload, and conserving client resources.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the way it used to be - before this PR. If you have any ideas on what delays to add, I can add them. I don't want to add them just like that, because it may increase initialization time

src/token-service/metadata-token-service.ts Outdated Show resolved Hide resolved
export class MetadataTokenService implements TokenService {
private readonly url: string;
private readonly opts: Options;
private token?: string;
private tokenExpired = 0;
private tokenTimeToRefresh = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not clear why do we need two different timestamps. Why can't one be derived out another?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two key time points for updating a token - when 10% of the lifetime has passed and when there are 15 minutes of lifetime left. To avoid repeating these calculations in different places (which can be a source of errors), it is easier to calculate them once when receiving a token

const res = await axios.get<{ access_token: string }>(this.url, this.opts);

// deduplicate real fetch token requests in any async case
if (!this.currentFetchToken) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't you invert the if statement and return existing promise early?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a more comprehensible code. Otherwise there will be two return statements

try {
this.token = await this.fetchToken();
} catch {
// nothing - use old token
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why there is no error handling?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need - we can continue to use the old token

const timeLeft = res.data.expires_in * 1000 - TOKEN_MINIMUM_LIFETIME_MARGIN_MS;

if (timeLeft <= 0) {
throw new Error('failed to fetch token: insufficient lifetime');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is wrong. The token we got is totally usable. Is this error retriable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is for the hypothetical case if a token with less than 15 min remaining lifetime is returned. Now it is not reproducible - as tokens with 10+ hours of reserve are returned

for (let i = 0; i < 5; i++) {
delete this.token;

for (let i = 0; i < MAX_ATTEMPTS_NUMBER_TO_GET_TOKEN_IN_INITIALIZE; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why there are retries only in initialize phase?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because if we already have a token and it has at least 15 min life left. In case of an error, there is no need to retry, we can continue to use the old token.

@@ -50,6 +50,7 @@
"import/no-extraneous-dependencies": ["error", {
"devDependencies": true
}],
"import/no-cycle": "off"
"import/no-cycle": "off",
"linebreak-style": "off"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turn this back on, please. And fix linebreaks in the IDE settings.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I'm developing on Windows, if I don't enable this option, I get an error on every line ) However, any windows git client converts crlf to lf during any git commit

Copy link
Contributor

@nikolaymatrosov nikolaymatrosov Aug 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said, you should fix it in the IDE settings.
Screenshot 2023-08-01 at 11 48 55

Copy link
Contributor Author

@Zork33 Zork33 Oct 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

а кто нибуть на практике пробовал так работать? потому что для windows crlf возникает на всех шагах - когда файлы из git приходят, когда редактируешь любым не специально настроенным редактором, генераторы кода тоже с таким переносом пишут. так что просто настройки IDE задачу увы не решают

@Zork33 Zork33 force-pushed the fix-metadata-token-service branch from 7256bcf to b47248b Compare August 1, 2023 09:41
package.json Outdated Show resolved Hide resolved
src/token-service/metadata-token-service.ts Outdated Show resolved Hide resolved
@@ -15,64 +21,93 @@ export class MetadataTokenService implements TokenService {
private readonly url: string;
private readonly opts: Options;
private token?: string;
private tokenExpiredAt = 0;
private tokenTimeToRefresh = 0;
private currentFetchToken?: Promise<string>;
Copy link
Contributor

@DavyJohnes DavyJohnes Aug 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest to use something like https://github.com/DirtyHairy/async-mutex in order to guarantee single fetch at any point of time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at this library. It is about mutexes and simophores, which doesn't seem solving my task. I need that one internal query is formed for several parallel queries, and all queries return the same result - promis sharing solves it

@esseswann
Copy link

Are there any chances this going to be merged soon?

@@ -50,6 +50,7 @@
"import/no-extraneous-dependencies": ["error", {
"devDependencies": true
}],
"import/no-cycle": "off"
"import/no-cycle": "off",
"linebreak-style": "off"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Zork33 давай, пожалуйста, вернем это правило, с учетом того что Коля выше описал.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

это правило ломает разработку под windows - lint начинает выглядеть вот так,
image

если его возвращать, то хорошо бы придумать как это обойти под виндой. вариант чтобы под виндой были просто lf переносы не встречал. при этом, как я уже говорил, git под виндой сам нормализует crlf -> lf при комите. Есть ли реальная проблема?

package.json Outdated
"lint": "eslint src config",
"test": "cross-env NODE_OPTIONS=\"--max-old-space-size=4096\" jest -c config/jest.ts --passWithNoTests",
"test:coverage": "jest -c config/jest.coverage.ts --passWithNoTests",
"lint": "eslint src config --fix",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не стоит, эта же команда и в CI выполняется, не очень хорошо что там после выполнения команды останутся изменения.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Понял. Согласен!

package.json Outdated
@@ -66,8 +66,9 @@
"typescript": "^4.5.4"
},
"scripts": {
"test": "cross-env NODE_OPTIONS=\"--max-old-space-size=4096\" jest -c config/jest.ts --passWithNoTests '.*\\.test\\.ts$'",
"lint": "eslint src config",
"test": "cross-env NODE_OPTIONS=\"--max-old-space-size=4096\" jest -c config/jest.ts --passWithNoTests",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--passWithNoTests уже можно смело убирать

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Похоже убирать не стоит - без нее тесты сыпятся на файле src/generated/yandex/cloud/loadtesting/agent/v1/test.ts Который понятно тестом не является, но под маску тестовых файлов подходит

Copy link
Contributor Author

@Zork33 Zork33 Oct 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Проверил и с этой опцией сыпятся. Так что добавил игнор папки src/generated

package.json Outdated
"test": "cross-env NODE_OPTIONS=\"--max-old-space-size=4096\" jest -c config/jest.ts --passWithNoTests '.*\\.test\\.ts$'",
"lint": "eslint src config",
"test": "cross-env NODE_OPTIONS=\"--max-old-space-size=4096\" jest -c config/jest.ts --passWithNoTests",
"test:coverage": "jest -c config/jest.coverage.ts --passWithNoTests",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А почему cross-env NODE_OPTIONS=\"--max-old-space-size=4096\" тут не нужно?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

верну. убрал для теста и забыл

await metadataTokenService.dispose();

expect(testLoggerFn.mock.calls)
.toEqual([
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тестировать логгирование компонента - это уже чересчур, как по мне :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

)) ну на самом деле это вроде как know how. то есть через проверку логов, фиксируется что произошло то что и ожидалась. в частности через проверку debug выдачи проверяется что параметры конструктора добрались куда должны были


/* istanbul ignore next */
// eslint-disable-next-line @typescript-eslint/no-namespace,import/export
export namespace MetadataTokenService {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А зачем неймспейс? Да и в целом, у тебя есть модуль с константами для metadata-token-service, кажется этим константам там и место?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Неймспейс появился для интерфейсов. Просто если объявить интерфейс Logger глобальным, то по правилам ts его может расширить интерфейс Logger из другого кода, если они вдруг встретятся. Вроде namespace для этого как раз и нужен

А константы туда попали по инерции. Перенесу их в consts


delete this.token;

for (let i = 1; ; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А почему у нас ретраи на фетч токена есть только в initialize()? Кажется что в getToken() тоже хочется ретраить?

Copy link
Contributor Author

@Zork33 Zork33 Oct 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Идея в том и есть комментарий в коде - что если есть активный токен, то ошибка get token не требует моментальных ретраев, и продолжает использоваться имеющийся токен. При этом факт ошибки учитывается при вычислении времени когда повторно сходить за токеном - это ретраи

// eslint-disable-next-line no-bitwise
const slotsCount = 1 << Math.min(i, INITIALIZE_BACKOFF_CEILING);
const maxDuration = slotsCount * INITIALIZE_BACKOFF_SLOT_DURATION;
const duration = Math.trunc(maxDuration * (1 - Math.random() * INITIALIZE_BACKOFF_UNCERTAIN_RATIO));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Зачем мы сами имплементируем ретраи, если можно просто заюзать axios-retry? Кажется в нем есть уже все нужные опции.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

да - axiios-retry можно использовать в initialize(). для getToken() он не подходит - так повторы не в цикле. предлагаю оставить как есть - уже протестировано и минус одна дополнительная зависимость

@@ -0,0 +1,149 @@
const DEFAULT_ENV_KEY = 'LOG_LEVEL';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А зачем мы свой логгер имплементим? Чем любой из доступных в npm не подходит?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ну я действовал по аналогии с ydb-sdk, чтобы не создавать лишние зависимости, проще иметь свой простой логер. Те логеры которые я знаю в npm довольно много зависимостей тянут и вариантов много. А так есть логер для простых тестов - консоль всегда есть. Кстати, для тестов в облаке я еще вот такой варант написал (https://github.com/Zork33/yandex-cloud-simple-logger) - может знаешь какую нить другую реализацию простого yc-логирования

// this additional logic ensures that by the end of advanceTimer(), all handlers including asynchronous
// handlers on timers will be completed
// eslint-disable-next-line @typescript-eslint/ban-types
await Promise.all(this.timeouts);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А вот это разве не тоже самое? https://jestjs.io/docs/jest-object#jestrunalltimersasync

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Судя по описанию это то что нужно. Правда появилось недавно и я пока не понял, что именно нужно обновить. Поставил jest 29.5.5 но при этом получаю такую ошибку
image

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Там есть подсказка
This function is not available when using legacy fake timers implementation.

Copy link
Contributor Author

@Zork33 Zork33 Oct 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ага - я ее видел. Но беглый поиск не дал ответа, что именно надо обновить. Потому вернусь к этом вопросу позже-

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

bug: Add to src/token-service/metadata-token-service.ts regular token updates
4 participants