-
Notifications
You must be signed in to change notification settings - Fork 14
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
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Не уверен, что схема с lastFetch
будет работать.
f9df471
to
7256bcf
Compare
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
export class MetadataTokenService implements TokenService { | ||
private readonly url: string; | ||
private readonly opts: Options; | ||
private token?: string; | ||
private tokenExpired = 0; | ||
private tokenTimeToRefresh = 0; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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++) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
а кто нибуть на практике пробовал так работать? потому что для windows crlf возникает на всех шагах - когда файлы из git приходят, когда редактируешь любым не специально настроенным редактором, генераторы кода тоже с таким переносом пишут. так что просто настройки IDE задачу увы не решают
7256bcf
to
b47248b
Compare
@@ -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>; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
Are there any chances this going to be merged soon? |
e10bb4e
to
1492b04
Compare
@@ -50,6 +50,7 @@ | |||
"import/no-extraneous-dependencies": ["error", { | |||
"devDependencies": true | |||
}], | |||
"import/no-cycle": "off" | |||
"import/no-cycle": "off", | |||
"linebreak-style": "off" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Zork33 давай, пожалуйста, вернем это правило, с учетом того что Коля выше описал.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
это правило ломает разработку под windows - lint начинает выглядеть вот так,
если его возвращать, то хорошо бы придумать как это обойти под виндой. вариант чтобы под виндой были просто 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", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Не стоит, эта же команда и в CI выполняется, не очень хорошо что там после выполнения команды останутся изменения.
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
--passWithNoTests
уже можно смело убирать
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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 Который понятно тестом не является, но под маску тестовых файлов подходит
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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\"
тут не нужно?
There was a problem hiding this comment.
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([ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Тестировать логгирование компонента - это уже чересчур, как по мне :)
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А зачем неймспейс? Да и в целом, у тебя есть модуль с константами для metadata-token-service
, кажется этим константам там и место?
There was a problem hiding this comment.
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++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А почему у нас ретраи на фетч токена есть только в initialize()
? Кажется что в getToken()
тоже хочется ретраить?
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Зачем мы сами имплементируем ретраи, если можно просто заюзать axios-retry
? Кажется в нем есть уже все нужные опции.
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А зачем мы свой логгер имплементим? Чем любой из доступных в npm
не подходит?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ага - я ее видел. Но беглый поиск не дал ответа, что именно надо обновить. Потому вернусь к этом вопросу позже-
dfd0fe5
to
c59ff46
Compare
e3d1694
to
213cbbb
Compare
close #130