diff --git a/back/src/domains/core/rome/adapters/DiagorienteAppellationsGateway.manual.test.ts b/back/src/domains/core/rome/adapters/DiagorienteAppellationsGateway.manual.test.ts index 3d89b2bc54..d79c148509 100644 --- a/back/src/domains/core/rome/adapters/DiagorienteAppellationsGateway.manual.test.ts +++ b/back/src/domains/core/rome/adapters/DiagorienteAppellationsGateway.manual.test.ts @@ -172,5 +172,28 @@ describe("DiagorienteAppellationsGateway", () => { }, parallelCalls * requestMinTime * 10 + 500, ); + + it("fallsback to empty array when duration is over maximum", async () => { + const config = AppConfig.createFromEnv(); + const maxDurationMs = 100; + const appellationsGatewayWithLowMaxDuration = + new DiagorienteAppellationsGateway( + createFetchSharedClient(diagorienteAppellationsRoutes, fetch), + cachingGateway, + { + clientId: config.diagorienteApiClientId, + clientSecret: config.diagorienteApiClientSecret, + }, + withNoCache, + maxDurationMs, + ); + + const appellations = + await appellationsGatewayWithLowMaxDuration.searchAppellations( + "design", + ); + + expect(appellations).toEqual([]); + }); }); }); diff --git a/back/src/domains/core/rome/adapters/DiagorienteAppellationsGateway.ts b/back/src/domains/core/rome/adapters/DiagorienteAppellationsGateway.ts index 0f5acab709..95379ded21 100644 --- a/back/src/domains/core/rome/adapters/DiagorienteAppellationsGateway.ts +++ b/back/src/domains/core/rome/adapters/DiagorienteAppellationsGateway.ts @@ -1,6 +1,8 @@ import Bottleneck from "bottleneck"; -import { AppellationDto, appellationCodeSchema } from "shared"; +import { AppellationDto, appellationCodeSchema, sleep } from "shared"; import { HttpClient } from "shared-routes"; +import { partnerNames } from "../../../../config/bootstrap/partnerNames"; +import { createLogger } from "../../../../utils/logger"; import { InMemoryCachingGateway } from "../../caching-gateway/adapters/InMemoryCachingGateway"; import { WithCache } from "../../caching-gateway/port/WithCache"; import { AppellationsGateway } from "../ports/AppellationsGateway"; @@ -20,6 +22,8 @@ export const requestMinTime = Math.floor( ONE_SECOND_MS / diagorienteMaxCallRatePerSeconds, ); +const logger = createLogger(__filename); + export class DiagorienteAppellationsGateway implements AppellationsGateway { #limiter = new Bottleneck({ reservoir: diagorienteMaxCallRatePerSeconds, @@ -30,6 +34,7 @@ export class DiagorienteAppellationsGateway implements AppellationsGateway { }); #withCache: WithCache; + #maxQueryDurationMs: number; constructor( private readonly httpClient: HttpClient, @@ -39,8 +44,10 @@ export class DiagorienteAppellationsGateway implements AppellationsGateway { clientSecret: string; }, withCache: WithCache, + maxQueryDurationMs = 700, ) { this.#withCache = withCache; + this.#maxQueryDurationMs = maxQueryDurationMs; } async searchAppellations(rawQuery: string): Promise { @@ -69,7 +76,31 @@ export class DiagorienteAppellationsGateway implements AppellationsGateway { ), }); - return this.#limiter.schedule(() => cachedSearchAppellations(rawQuery)); + return this.#limiter.schedule(() => { + const apiCallPromise = cachedSearchAppellations(rawQuery); + + return Promise.race([ + apiCallPromise, + sleep(this.#maxQueryDurationMs).then(() => { + logger.warn({ + partnerApiCall: { + partnerName: partnerNames.diagoriente, + durationInMs: this.#maxQueryDurationMs, + route: diagorienteAppellationsRoutes.searchAppellations, + response: { + kind: "failure", + status: 504, + body: { + message: `Timeout on immersion facilitée side - more than ${this.#maxQueryDurationMs} ms to response`, + }, + }, + }, + }); + + return []; + }), + ]); + }); } public getAccessToken(): Promise { diff --git a/back/src/domains/core/rome/adapters/InMemoryAppellationsGateway.ts b/back/src/domains/core/rome/adapters/InMemoryAppellationsGateway.ts index 1142bcfd40..4cc9abbff0 100644 --- a/back/src/domains/core/rome/adapters/InMemoryAppellationsGateway.ts +++ b/back/src/domains/core/rome/adapters/InMemoryAppellationsGateway.ts @@ -1,10 +1,8 @@ -import { AppellationDto, sleep, toLowerCaseWithoutDiacritics } from "shared"; +import { AppellationDto, toLowerCaseWithoutDiacritics } from "shared"; import { AppellationsGateway } from "../ports/AppellationsGateway"; export class InMemoryAppellationsGateway implements AppellationsGateway { public async searchAppellations(query: string): Promise { - if (this.#delayInMs > 0) await sleep(this.#delayInMs); - return this.#nextResults.filter((appellation) => toLowerCaseWithoutDiacritics(appellation.appellationLabel).includes( toLowerCaseWithoutDiacritics(query), @@ -16,11 +14,5 @@ export class InMemoryAppellationsGateway implements AppellationsGateway { this.#nextResults = nextResult; } - public setDelayInMs(delayInMs: number) { - this.#delayInMs = delayInMs; - } - - #delayInMs = 0; - #nextResults: AppellationDto[] = []; } diff --git a/back/src/domains/core/rome/use-cases/AppellationSearch.manual.test.ts b/back/src/domains/core/rome/use-cases/AppellationSearch.manual.test.ts deleted file mode 100644 index 42d046f6d6..0000000000 --- a/back/src/domains/core/rome/use-cases/AppellationSearch.manual.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { AppellationAndRomeDto, expectToEqual } from "shared"; -import { InMemoryUowPerformer } from "../../unit-of-work/adapters/InMemoryUowPerformer"; -import { createInMemoryUow } from "../../unit-of-work/adapters/createInMemoryUow"; -import { InMemoryAppellationsGateway } from "../adapters/InMemoryAppellationsGateway"; -import { InMemoryRomeRepository } from "../adapters/InMemoryRomeRepository"; -import { AppellationSearch } from "./AppellationSearch"; - -describe("AppellationSearch Manual test, which take to long to run in CI", () => { - let romeRepo: InMemoryRomeRepository; - let appellationsGateway: InMemoryAppellationsGateway; - let appellationSearch: AppellationSearch; - - beforeEach(() => { - romeRepo = new InMemoryRomeRepository(); - appellationsGateway = new InMemoryAppellationsGateway(); - appellationSearch = new AppellationSearch( - new InMemoryUowPerformer({ - ...createInMemoryUow(), - romeRepository: romeRepo, - }), - appellationsGateway, - ); - }); - - it("has a fallback when diagoriente searchAppellations takes too long", async () => { - appellationsGateway.setNextSearchAppellationsResult([ - { appellationCode: "19364", appellationLabel: "Secrétaire" }, - ]); - - const appellation: AppellationAndRomeDto = { - romeCode: "M1607", - appellationCode: "19364", - appellationLabel: "Secrétaire", - romeLabel: "Secrétariat", - }; - - const appellationFallback: AppellationAndRomeDto = { - romeCode: "M1607", - appellationCode: "19367", - appellationLabel: - "Secrétaire bureautique spécialisé / spécialisée - FALLBACK", - romeLabel: "Secrétariat", - }; - - romeRepo.appellations = [appellation, appellationFallback]; - - appellationsGateway.setDelayInMs(680); - - expectToEqual( - await appellationSearch.execute({ - searchText: "secretaire", - fetchAppellationsFromNaturalLanguage: true, - }), - [ - { - appellation: appellation, - matchRanges: [{ startIndexInclusive: 0, endIndexExclusive: 10 }], - }, - ], - ); - - appellationsGateway.setDelayInMs(720); - - expectToEqual( - await appellationSearch.execute({ - searchText: "secretaire", - fetchAppellationsFromNaturalLanguage: true, - }), - [ - { - appellation: appellation, - matchRanges: [{ startIndexInclusive: 0, endIndexExclusive: 10 }], - }, - { - appellation: appellationFallback, - matchRanges: [{ startIndexInclusive: 0, endIndexExclusive: 10 }], - }, - ], - ); - }); -}); diff --git a/back/src/domains/core/rome/use-cases/AppellationSearch.ts b/back/src/domains/core/rome/use-cases/AppellationSearch.ts index 86be37afc8..162b71a857 100644 --- a/back/src/domains/core/rome/use-cases/AppellationSearch.ts +++ b/back/src/domains/core/rome/use-cases/AppellationSearch.ts @@ -2,17 +2,14 @@ import { AppellationAndRomeDto, AppellationMatchDto, ROME_AND_APPELLATION_MIN_SEARCH_TEXT_LENGTH, - sleep, zStringMinLength1, } from "shared"; import { z } from "zod"; -import { partnerNames } from "../../../../config/bootstrap/partnerNames"; import { createLogger } from "../../../../utils/logger"; import { findMatchRanges } from "../../../../utils/textSearch"; import { TransactionalUseCase } from "../../UseCase"; import { UnitOfWork } from "../../unit-of-work/ports/UnitOfWork"; import { UnitOfWorkPerformer } from "../../unit-of-work/ports/UnitOfWorkPerformer"; -import { diagorienteAppellationsRoutes } from "../adapters/DiagorienteAppellationsGateway.routes"; import { AppellationsGateway } from "../ports/AppellationsGateway"; const logger = createLogger(__filename); @@ -80,32 +77,8 @@ export class AppellationSearch extends TransactionalUseCase< uow: UnitOfWork, searchText: string, ): Promise { - const apiCallPromise = - this.#appellationsGateway.searchAppellations(searchText); - - const maxDurationMs = 700; - - const appellations = await Promise.race([ - apiCallPromise, - sleep(maxDurationMs).then(() => { - logger.warn({ - partnerApiCall: { - partnerName: partnerNames.diagoriente, - durationInMs: maxDurationMs, - route: diagorienteAppellationsRoutes.searchAppellations, - response: { - kind: "failure", - status: 504, - body: { - message: `Timeout on immersion facilitée side - more than ${maxDurationMs} ms to response`, - }, - }, - }, - }); - - return []; - }), - ]); + const appellations = + await this.#appellationsGateway.searchAppellations(searchText); if (appellations.length === 0) return [];