bug: fix metadata-token-service.ts#135
Conversation
nikolaymatrosov
left a comment
There was a problem hiding this comment.
Не уверен, что схема с 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.
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.
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.
превел на ! там где это подходит
| // 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.
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.
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
| // eslint-disable-next-line no-await-in-loop | ||
| this.token = await this.fetchToken(); | ||
| break; | ||
|
|
There was a problem hiding this comment.
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.
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
| private readonly opts: Options; | ||
| private token?: string; | ||
| private tokenExpired = 0; | ||
| private tokenTimeToRefresh = 0; |
There was a problem hiding this comment.
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.
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.
Why don't you invert the if statement and return existing promise early?
There was a problem hiding this comment.
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.
Why there is no error handling?
There was a problem hiding this comment.
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.
That is wrong. The token we got is totally usable. Is this error retriable?
There was a problem hiding this comment.
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.
Why there are retries only in initialize phase?
There was a problem hiding this comment.
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.
| }], | ||
| "import/no-cycle": "off" | ||
| "import/no-cycle": "off", | ||
| "linebreak-style": "off" |
There was a problem hiding this comment.
Turn this back on, please. And fix linebreaks in the IDE settings.
There was a problem hiding this comment.
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.
а кто нибуть на практике пробовал так работать? потому что для windows crlf возникает на всех шагах - когда файлы из git приходят, когда редактируешь любым не специально настроенным редактором, генераторы кода тоже с таким переносом пишут. так что просто настройки IDE задачу увы не решают
7256bcf to
b47248b
Compare
| private token?: string; | ||
| private tokenExpiredAt = 0; | ||
| private tokenTimeToRefresh = 0; | ||
| private currentFetchToken?: Promise<string>; |
There was a problem hiding this comment.
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.
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
| }], | ||
| "import/no-cycle": "off" | ||
| "import/no-cycle": "off", | ||
| "linebreak-style": "off" |
There was a problem hiding this comment.
@Zork33 давай, пожалуйста, вернем это правило, с учетом того что Коля выше описал.
There was a problem hiding this comment.
это правило ломает разработку под 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.
Не стоит, эта же команда и в CI выполняется, не очень хорошо что там после выполнения команды останутся изменения.
package.json
Outdated
| "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.
--passWithNoTests уже можно смело убирать
There was a problem hiding this comment.
Похоже убирать не стоит - без нее тесты сыпятся на файле src/generated/yandex/cloud/loadtesting/agent/v1/test.ts Который понятно тестом не является, но под маску тестовых файлов подходит
There was a problem hiding this comment.
Проверил и с этой опцией сыпятся. Так что добавил игнор папки 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.
А почему cross-env NODE_OPTIONS=\"--max-old-space-size=4096\" тут не нужно?
| await metadataTokenService.dispose(); | ||
|
|
||
| expect(testLoggerFn.mock.calls) | ||
| .toEqual([ |
There was a problem hiding this comment.
Тестировать логгирование компонента - это уже чересчур, как по мне :)
There was a problem hiding this comment.
)) ну на самом деле это вроде как 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.
А зачем неймспейс? Да и в целом, у тебя есть модуль с константами для metadata-token-service, кажется этим константам там и место?
There was a problem hiding this comment.
Неймспейс появился для интерфейсов. Просто если объявить интерфейс Logger глобальным, то по правилам ts его может расширить интерфейс Logger из другого кода, если они вдруг встретятся. Вроде namespace для этого как раз и нужен
А константы туда попали по инерции. Перенесу их в consts
|
|
||
| delete this.token; | ||
|
|
||
| for (let i = 1; ; i++) { |
There was a problem hiding this comment.
А почему у нас ретраи на фетч токена есть только в initialize()? Кажется что в getToken() тоже хочется ретраить?
There was a problem hiding this comment.
Идея в том и есть комментарий в коде - что если есть активный токен, то ошибка 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.
Зачем мы сами имплементируем ретраи, если можно просто заюзать axios-retry? Кажется в нем есть уже все нужные опции.
There was a problem hiding this comment.
да - axiios-retry можно использовать в initialize(). для getToken() он не подходит - так повторы не в цикле. предлагаю оставить как есть - уже протестировано и минус одна дополнительная зависимость
| @@ -0,0 +1,149 @@ | |||
| const DEFAULT_ENV_KEY = 'LOG_LEVEL'; | |||
There was a problem hiding this comment.
А зачем мы свой логгер имплементим? Чем любой из доступных в npm не подходит?
There was a problem hiding this comment.
Ну я действовал по аналогии с 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.
А вот это разве не тоже самое? https://jestjs.io/docs/jest-object#jestrunalltimersasync
There was a problem hiding this comment.
Там есть подсказка
This function is not available when using legacy fake timers implementation.
There was a problem hiding this comment.
Ага - я ее видел. Но беглый поиск не дал ответа, что именно надо обновить. Потому вернусь к этом вопросу позже-
dfd0fe5 to
c59ff46
Compare
e3d1694 to
213cbbb
Compare


close #130