diff --git a/api/apps/api/config/custom-environment-variables.json b/api/apps/api/config/custom-environment-variables.json index fe963c1f70..2e402f8d68 100644 --- a/api/apps/api/config/custom-environment-variables.json +++ b/api/apps/api/config/custom-environment-variables.json @@ -28,6 +28,9 @@ "host": "REDIS_HOST" } }, + "api": { + "url": "API_SERVICE_URL" + }, "storage": { "sharedFileStorage": { "localPath": "API_SHARED_FILE_STORAGE_LOCAL_PATH" diff --git a/api/apps/api/config/default.json b/api/apps/api/config/default.json index 28493b6643..308df982c8 100644 --- a/api/apps/api/config/default.json +++ b/api/apps/api/config/default.json @@ -13,6 +13,9 @@ }, "concurrency": 50 }, + "api": { + "url": "http://api:3000" + }, "jobOptions":{ "removeOnComplete": 100, "removeOnFail": 1000, diff --git a/api/apps/api/src/modules/scenarios/marxan-run/assets.service.spec.ts b/api/apps/api/src/modules/scenarios/marxan-run/assets.service.spec.ts new file mode 100644 index 0000000000..15a99cd1ea --- /dev/null +++ b/api/apps/api/src/modules/scenarios/marxan-run/assets.service.spec.ts @@ -0,0 +1,70 @@ +import { PromiseType } from 'utility-types'; +import { Test } from '@nestjs/testing'; +import { apiUrlToken, AssetsService } from './assets.service'; +import { IoSettings, ioSettingsToken } from './io-settings'; + +let fixtures: PromiseType>; +let service: AssetsService; + +beforeEach(async () => { + fixtures = await getFixtures(); + + service = fixtures.getAssetsService(); +}); + +it(`should return valid config`, async () => { + const random = fixtures.random; + expect(await service.forScenario(`scenario-${random}`)).toStrictEqual([ + { + url: `https://api-endpoint${random}.test:3000/api/v1/marxan-run/scenarios/scenario-${random}/marxan/dat/input.dat`, + relativeDestination: `input.dat`, + }, + { + url: `https://api-endpoint${random}.test:3000/api/v1/marxan-run/scenarios/scenario-${random}/marxan/dat/pu.dat`, + relativeDestination: `inputDir${random}/pu-name-file${random}`, + }, + { + url: `https://api-endpoint${random}.test:3000/api/v1/marxan-run/scenarios/scenario-${random}/marxan/dat/bound.dat`, + relativeDestination: `inputDir${random}/bound-name-file${random}`, + }, + { + url: `https://api-endpoint${random}.test:3000/api/v1/marxan-run/scenarios/scenario-${random}/marxan/dat/spec.dat`, + relativeDestination: `inputDir${random}/spec-name-file${random}`, + }, + { + url: `https://api-endpoint${random}.test:3000/api/v1/marxan-run/scenarios/scenario-${random}/marxan/dat/puvspr.dat`, + relativeDestination: `inputDir${random}/puv-spr-name-file${random}`, + }, + ]); +}); + +async function getFixtures() { + const random = Math.random().toString(36); + const ioSettings: IoSettings = { + BOUNDNAME: `bound-name-file${random}`, + INPUTDIR: `inputDir${random}`, + OUTPUTDIR: `OUTPUTDIR.file${random}`, + PUNAME: `pu-name-file${random}`, + PUVSPRNAME: `puv-spr-name-file${random}`, + SPECNAME: `spec-name-file${random}`, + }; + const testingModule = await Test.createTestingModule({ + providers: [ + { + provide: ioSettingsToken, + useValue: ioSettings, + }, + { + provide: apiUrlToken, + useValue: `https://api-endpoint${random}.test:3000`, + }, + AssetsService, + ], + }).compile(); + return { + random, + getAssetsService() { + return testingModule.get(AssetsService); + }, + }; +} diff --git a/api/apps/api/src/modules/scenarios/marxan-run/assets.service.ts b/api/apps/api/src/modules/scenarios/marxan-run/assets.service.ts new file mode 100644 index 0000000000..e12da8d976 --- /dev/null +++ b/api/apps/api/src/modules/scenarios/marxan-run/assets.service.ts @@ -0,0 +1,45 @@ +import { Assets } from '@marxan/scenario-run-queue'; +import { FactoryProvider, Inject, Injectable } from '@nestjs/common'; +import { AppConfig } from '@marxan-api/utils/config.utils'; +import { IoSettings, ioSettingsToken } from './io-settings'; + +export const apiUrlToken = Symbol('api url token'); +export const apiUrlProvider: FactoryProvider = { + provide: apiUrlToken, + useFactory: () => AppConfig.get('api.url'), +}; + +@Injectable() +export class AssetsService { + constructor( + @Inject(ioSettingsToken) + private readonly settings: IoSettings, + @Inject(apiUrlToken) + private readonly apiUrlToken: string, + ) {} + + async forScenario(id: string): Promise { + return [ + { + url: `${this.apiUrlToken}/api/v1/marxan-run/scenarios/${id}/marxan/dat/input.dat`, + relativeDestination: 'input.dat', + }, + { + url: `${this.apiUrlToken}/api/v1/marxan-run/scenarios/${id}/marxan/dat/pu.dat`, + relativeDestination: `${this.settings.INPUTDIR}/${this.settings.PUNAME}`, + }, + { + url: `${this.apiUrlToken}/api/v1/marxan-run/scenarios/${id}/marxan/dat/bound.dat`, + relativeDestination: `${this.settings.INPUTDIR}/${this.settings.BOUNDNAME}`, + }, + { + url: `${this.apiUrlToken}/api/v1/marxan-run/scenarios/${id}/marxan/dat/spec.dat`, + relativeDestination: `${this.settings.INPUTDIR}/${this.settings.SPECNAME}`, + }, + { + url: `${this.apiUrlToken}/api/v1/marxan-run/scenarios/${id}/marxan/dat/puvspr.dat`, + relativeDestination: `${this.settings.INPUTDIR}/${this.settings.PUVSPRNAME}`, + }, + ]; + } +} diff --git a/api/apps/api/src/modules/scenarios/marxan-run/index.ts b/api/apps/api/src/modules/scenarios/marxan-run/index.ts new file mode 100644 index 0000000000..23abff2790 --- /dev/null +++ b/api/apps/api/src/modules/scenarios/marxan-run/index.ts @@ -0,0 +1,9 @@ +export { + RunService, + notFound, + NotFound, + runQueueProvider, + runQueueEventsProvider, +} from './run.service'; +export { InputParameterFileProvider } from './input-parameter-file.provider'; +export { MarxanRunModule } from './marxan-run.module'; diff --git a/api/apps/api/src/modules/scenarios/input-parameter-file.provider.spec.ts b/api/apps/api/src/modules/scenarios/marxan-run/input-parameter-file.provider.spec.ts similarity index 86% rename from api/apps/api/src/modules/scenarios/input-parameter-file.provider.spec.ts rename to api/apps/api/src/modules/scenarios/marxan-run/input-parameter-file.provider.spec.ts index 3c6e9dc917..cf20161a19 100644 --- a/api/apps/api/src/modules/scenarios/input-parameter-file.provider.spec.ts +++ b/api/apps/api/src/modules/scenarios/marxan-run/input-parameter-file.provider.spec.ts @@ -1,11 +1,10 @@ import { PromiseType } from 'utility-types'; import { Test } from '@nestjs/testing'; -import { - InputParameterFileProvider, - ioSettingsToken, -} from './input-parameter-file.provider'; -import { JobStatus, Scenario, ScenarioType } from './scenario.api.entity'; -import { ScenariosCrudService } from './scenarios-crud.service'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { JobStatus, Scenario, ScenarioType } from '../scenario.api.entity'; +import { InputParameterFileProvider } from './input-parameter-file.provider'; +import { ioSettingsToken } from './io-settings'; jest.useFakeTimers('modern').setSystemTime(new Date('2020-01-01').getTime()); @@ -119,10 +118,16 @@ OUTPUTDIR output`); }); async function getFixtures() { - class FakeScenario implements Pick { + class FakeScenario implements Pick, 'findOne'> { db: Record = {}; - async getById(scenarioId: string): Promise { + async findOne(scenarioId: any, ...rest: any[]): Promise { + expect(rest).toStrictEqual([ + { + relations: ['project', 'project.organization'], + }, + ]); + if (typeof scenarioId !== 'string') fail(); return this.db[scenarioId]; } } @@ -131,9 +136,10 @@ async function getFixtures() { imports: [], providers: [ InputParameterFileProvider, + FakeScenario, { - provide: ScenariosCrudService, - useClass: FakeScenario, + provide: getRepositoryToken(Scenario), + useExisting: FakeScenario, }, { provide: ioSettingsToken, @@ -148,7 +154,7 @@ async function getFixtures() { }, ], }).compile(); - const fakeRepo: FakeScenario = testingModule.get(ScenariosCrudService); + const fakeRepo: FakeScenario = testingModule.get(FakeScenario); return { async hasInDb(scenario: Scenario) { diff --git a/api/apps/api/src/modules/scenarios/input-parameter-file.provider.ts b/api/apps/api/src/modules/scenarios/marxan-run/input-parameter-file.provider.ts similarity index 82% rename from api/apps/api/src/modules/scenarios/input-parameter-file.provider.ts rename to api/apps/api/src/modules/scenarios/marxan-run/input-parameter-file.provider.ts index 1db4f2224b..18e8fec295 100644 --- a/api/apps/api/src/modules/scenarios/input-parameter-file.provider.ts +++ b/api/apps/api/src/modules/scenarios/marxan-run/input-parameter-file.provider.ts @@ -1,18 +1,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { omit, pick } from 'lodash'; -import { isDefined } from '@marxan/utils'; -import { ScenariosCrudService } from './scenarios-crud.service'; - -export const ioSettingsToken = Symbol('Marxan IO settings token'); - -export interface IoSettings { - INPUTDIR: string; - PUNAME: string; - SPECNAME: string; - PUVSPRNAME: string; - BOUNDNAME: string; - OUTPUTDIR: string; -} +import { Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { assertDefined, isDefined } from '@marxan/utils'; +import { Scenario } from '../scenario.api.entity'; +import { IoSettings, ioSettingsToken } from './io-settings'; class InputParameterFile { private ioSettingsKeys: (keyof IoSettings)[] = [ @@ -78,15 +70,17 @@ class InputParameterFile { @Injectable() export class InputParameterFileProvider { constructor( - private readonly scenariosService: ScenariosCrudService, + @InjectRepository(Scenario) + private readonly scenarioRepository: Repository, @Inject(ioSettingsToken) private readonly ioSettings: IoSettings, ) {} async getInputParameterFile(scenarioId: string): Promise { - const scenario = await this.scenariosService.getById(scenarioId, { - include: ['project', 'project.organization'], + const scenario = await this.scenarioRepository.findOne(scenarioId, { + relations: ['project', 'project.organization'], }); + assertDefined(scenario); const inputParameterFile = new InputParameterFile( this.ioSettings, scenario.boundaryLengthModifier, diff --git a/api/apps/api/src/modules/scenarios/marxan-run/io-settings.ts b/api/apps/api/src/modules/scenarios/marxan-run/io-settings.ts new file mode 100644 index 0000000000..288a8a96f0 --- /dev/null +++ b/api/apps/api/src/modules/scenarios/marxan-run/io-settings.ts @@ -0,0 +1,24 @@ +import { AppConfig } from '@marxan-api/utils/config.utils'; +import { assertDefined } from '@marxan/utils'; + +export const ioSettingsToken = Symbol('Marxan IO settings token'); + +export interface IoSettings { + INPUTDIR: string; + PUNAME: string; + SPECNAME: string; + PUVSPRNAME: string; + BOUNDNAME: string; + OUTPUTDIR: string; +} + +export const ioSettingsProvider = { + provide: ioSettingsToken, + useFactory: () => { + const config = AppConfig.get( + 'marxan.inputFiles.inputDat.ioSettings', + ); + assertDefined(config); + return config; + }, +}; diff --git a/api/apps/api/src/modules/scenarios/marxan-run/marxan-run.service.ts b/api/apps/api/src/modules/scenarios/marxan-run/marxan-files.service.ts similarity index 90% rename from api/apps/api/src/modules/scenarios/marxan-run/marxan-run.service.ts rename to api/apps/api/src/modules/scenarios/marxan-run/marxan-files.service.ts index 191cebe95f..394bf260d6 100644 --- a/api/apps/api/src/modules/scenarios/marxan-run/marxan-run.service.ts +++ b/api/apps/api/src/modules/scenarios/marxan-run/marxan-files.service.ts @@ -2,12 +2,12 @@ import { Injectable } from '@nestjs/common'; import * as stream from 'stream'; import { CostSurfaceViewService } from '../cost-surface-readmodel/cost-surface-view.service'; -import { InputParameterFileProvider } from '../input-parameter-file.provider'; import { PuvsprDatService } from '../input-files/puvspr.dat.service'; import { BoundDatService } from '../input-files/bound.dat.service'; +import { InputParameterFileProvider } from './input-parameter-file.provider'; @Injectable() -export class MarxanRunService { +export class MarxanFilesService { constructor( private readonly costSurfaceService: CostSurfaceViewService, private readonly inputParameterFileProvider: InputParameterFileProvider, diff --git a/api/apps/api/src/modules/scenarios/marxan-run/marxan-run.controller.ts b/api/apps/api/src/modules/scenarios/marxan-run/marxan-run.controller.ts index e3fb97ecf5..be32f9377c 100644 --- a/api/apps/api/src/modules/scenarios/marxan-run/marxan-run.controller.ts +++ b/api/apps/api/src/modules/scenarios/marxan-run/marxan-run.controller.ts @@ -16,12 +16,12 @@ import { import { Response } from 'express'; import { apiGlobalPrefixes } from '@marxan-api/api.config'; import { XApiGuard } from '@marxan-api/guards/x-api.guard'; -import { MarxanRunService } from './marxan-run.service'; +import { MarxanFilesService } from './marxan-files.service'; @UseGuards(XApiGuard) @Controller(`${apiGlobalPrefixes.v1}/marxan-run/scenarios`) export class MarxanRunController { - constructor(private readonly service: MarxanRunService) {} + constructor(private readonly service: MarxanFilesService) {} @Header('Content-Type', 'text/csv') @ApiOkResponse({ diff --git a/api/apps/api/src/modules/scenarios/marxan-run/marxan-run.module.ts b/api/apps/api/src/modules/scenarios/marxan-run/marxan-run.module.ts new file mode 100644 index 0000000000..a4a49ca46e --- /dev/null +++ b/api/apps/api/src/modules/scenarios/marxan-run/marxan-run.module.ts @@ -0,0 +1,44 @@ +import { Module } from '@nestjs/common'; +import { MarxanRunController } from './marxan-run.controller'; +import { MarxanFilesService } from './marxan-files.service'; +import { + runQueueEventsProvider, + runQueueProvider, + RunService, +} from './run.service'; +import { apiUrlProvider, AssetsService } from './assets.service'; +import { ioSettingsProvider } from './io-settings'; +import { InputParameterFileProvider } from './input-parameter-file.provider'; +import { QueueModule } from '@marxan-api/modules/queue'; +import { ApiEventsModule } from '@marxan-api/modules/api-events/api-events.module'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Scenario } from '../scenario.api.entity'; +import { CostSurfaceViewModule } from '../cost-surface-readmodel/cost-surface-view.module'; +import { SpecDatModule } from '@marxan-api/modules/scenarios/input-files/spec.dat.module'; +import { PuvsprDatModule } from '@marxan-api/modules/scenarios/input-files/puvspr.dat.module'; +import { BoundDatModule } from '@marxan-api/modules/scenarios/input-files/bound.dat.module'; + +@Module({ + imports: [ + QueueModule.register(), + ApiEventsModule, + TypeOrmModule.forFeature([Scenario]), + CostSurfaceViewModule, + SpecDatModule, + PuvsprDatModule, + BoundDatModule, + ], + providers: [ + MarxanFilesService, + runQueueProvider, + runQueueEventsProvider, + RunService, + AssetsService, + ioSettingsProvider, + apiUrlProvider, + InputParameterFileProvider, + ], + controllers: [MarxanRunController], + exports: [RunService, InputParameterFileProvider], +}) +export class MarxanRunModule {} diff --git a/api/apps/api/src/modules/scenarios/run.service.spec.ts b/api/apps/api/src/modules/scenarios/marxan-run/run.service.spec.ts similarity index 85% rename from api/apps/api/src/modules/scenarios/run.service.spec.ts rename to api/apps/api/src/modules/scenarios/marxan-run/run.service.spec.ts index 273169fec3..a658237375 100644 --- a/api/apps/api/src/modules/scenarios/run.service.spec.ts +++ b/api/apps/api/src/modules/scenarios/marxan-run/run.service.spec.ts @@ -1,18 +1,19 @@ import { EventEmitter } from 'events'; import { PromiseType } from 'utility-types'; -import { right, left } from 'fp-ts/Either'; +import { left, right } from 'fp-ts/Either'; import waitForExpect from 'wait-for-expect'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Test } from '@nestjs/testing'; import { API_EVENT_KINDS } from '@marxan/api-events'; +import { ApiEventsService } from '@marxan-api/modules/api-events/api-events.service'; +import { Scenario } from '../scenario.api.entity'; import { notFound, runEventsToken, runQueueToken, RunService, -} from '@marxan-api/modules/scenarios/run.service'; -import { ApiEventsService } from '@marxan-api/modules/api-events/api-events.service'; -import { Scenario } from './scenario.api.entity'; +} from './run.service'; +import { AssetsService } from './assets.service'; let fixtures: PromiseType>; let runService: RunService; @@ -24,6 +25,8 @@ beforeEach(async () => { test(`scheduling job`, async () => { fixtures.setupMocksForSchedulingJobs(); + // given + fixtures.GivenAssetsAvailable(); // when await runService.run('scenario-1'); @@ -34,6 +37,18 @@ test(`scheduling job`, async () => { fixtures.ThenShouldAddJob(); }); +test(`scheduling job for scenario without assets`, async () => { + fixtures.setupMocksForSchedulingJobs(); + // given + fixtures.GivenAssetsNotAvailable(); + + // when + const result = runService.run('scenario-1'); + + // then + await expect(result).rejects.toBeDefined(); +}); + test(`canceling job`, async () => { fixtures.GivenANotCancellableJobInQueue(); @@ -104,6 +119,9 @@ async function getFixtures() { const fakeScenarioRepo = { update: throwingMock(), }; + const fakeAssets = { + forScenario: jest.fn(), + }; const testingModule = await Test.createTestingModule({ providers: [ { @@ -122,6 +140,10 @@ async function getFixtures() { provide: getRepositoryToken(Scenario), useValue: fakeScenarioRepo, }, + { + provide: AssetsService, + useValue: fakeAssets, + }, RunService, ], }).compile(); @@ -159,6 +181,12 @@ async function getFixtures() { scenarioId: 'other-scenario', }, }, + scenarioAssets: [ + { + url: 'url-value', + relativeDestination: 'relativeDestination-value', + }, + ], getRunService() { return testingModule.get(RunService); }, @@ -220,6 +248,7 @@ async function getFixtures() { expect(fixtures.fakeQueue.add).toBeCalledTimes(1); expect(fixtures.fakeQueue.add).toBeCalledWith(`run-scenario`, { scenarioId: `scenario-1`, + assets: this.scenarioAssets, }); }, async ThenEventCreated(kind: API_EVENT_KINDS) { @@ -248,5 +277,17 @@ async function getFixtures() { }; }); }, + GivenAssetsAvailable() { + fakeAssets.forScenario.mockImplementation((id) => { + expect(id).toBe(`scenario-1`); + return this.scenarioAssets; + }); + }, + GivenAssetsNotAvailable() { + fakeAssets.forScenario.mockImplementation((id) => { + expect(id).toBe(`scenario-1`); + return undefined; + }); + }, }; } diff --git a/api/apps/api/src/modules/scenarios/run.service.ts b/api/apps/api/src/modules/scenarios/marxan-run/run.service.ts similarity index 87% rename from api/apps/api/src/modules/scenarios/run.service.ts rename to api/apps/api/src/modules/scenarios/marxan-run/run.service.ts index ed1853bf22..7cb32348dc 100644 --- a/api/apps/api/src/modules/scenarios/run.service.ts +++ b/api/apps/api/src/modules/scenarios/marxan-run/run.service.ts @@ -4,14 +4,15 @@ import { FactoryProvider, Inject, Injectable } from '@nestjs/common'; import { Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { assertDefined, isDefined } from '@marxan/utils'; +import { JobData, ProgressData, queueName } from '@marxan/scenario-run-queue'; import { ApiEventsService } from '@marxan-api/modules/api-events/api-events.service'; import { API_EVENT_KINDS } from '@marxan/api-events'; import { QueueBuilder, QueueEventsBuilder } from '@marxan-api/modules/queue'; -import { Scenario } from '@marxan-api/modules/scenarios/scenario.api.entity'; +import { Scenario } from '../scenario.api.entity'; +import { AssetsService } from './assets.service'; export const runQueueToken = Symbol('run queue token'); export const runEventsToken = Symbol('run events token'); -export const queueName = 'scenario-run-queue'; export const runQueueProvider: FactoryProvider> = { provide: runQueueToken, useFactory: (queueBuilder: QueueBuilder) => { @@ -27,15 +28,15 @@ export const runQueueEventsProvider: FactoryProvider = { inject: [QueueEventsBuilder], }; -type JobData = { - scenarioId: string; -}; - export const notFound = Symbol('not found'); export type NotFound = typeof notFound; @Injectable() export class RunService { + private canceledProgressData: ProgressData = { + canceled: true, + }; + constructor( @Inject(runQueueToken) private readonly queue: Queue, @@ -44,14 +45,18 @@ export class RunService { private readonly apiEvents: ApiEventsService, @InjectRepository(Scenario) private readonly scenarios: Repository, + private readonly assets: AssetsService, ) { queueEvents.on(`completed`, ({ jobId }) => this.handleFinished(jobId)); queueEvents.on(`failed`, ({ jobId }) => this.handleFailed(jobId)); } async run(scenarioId: string): Promise { + const assets = await this.assets.forScenario(scenarioId); + assertDefined(assets); await this.queue.add(`run-scenario`, { scenarioId, + assets, }); await this.scenarios.update(scenarioId, { ranAtLeastOnce: true, @@ -73,9 +78,7 @@ export class RunService { if (!isDefined(scenarioJob)) return left(notFound); if (await scenarioJob.isActive()) - await scenarioJob.updateProgress({ - canceled: true, - }); + await scenarioJob.updateProgress(this.canceledProgressData); else if (await scenarioJob.isWaiting()) await scenarioJob.remove(); return right(void 0); diff --git a/api/apps/api/src/modules/scenarios/scenarios.module.ts b/api/apps/api/src/modules/scenarios/scenarios.module.ts index 2f23c3737d..2801ae4c29 100644 --- a/api/apps/api/src/modules/scenarios/scenarios.module.ts +++ b/api/apps/api/src/modules/scenarios/scenarios.module.ts @@ -1,4 +1,4 @@ -import { forwardRef, Module, HttpModule } from '@nestjs/common'; +import { forwardRef, HttpModule, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CqrsModule } from '@nestjs/cqrs'; @@ -28,28 +28,12 @@ import { import { ScenarioSolutionSerializer } from './dto/scenario-solution.serializer'; import { CostSurfaceViewModule } from './cost-surface-readmodel/cost-surface-view.module'; import { PlanningUnitsProtectionLevelModule } from '@marxan-api/modules/planning-units-protection-level'; -import { - InputParameterFileProvider, - IoSettings, - ioSettingsToken, -} from './input-parameter-file.provider'; -import { assertDefined } from '@marxan/utils'; -import { AppConfig } from '@marxan-api/utils/config.utils'; -import { QueueModule } from '@marxan-api/modules/queue/queue.module'; -import { ApiEventsModule } from '@marxan-api/modules/api-events/api-events.module'; import { SpecDatModule } from './input-files/spec.dat.module'; import { PuvsprDatModule } from './input-files/puvspr.dat.module'; -import { - runQueueEventsProvider, - runQueueProvider, - RunService, -} from './run.service'; - -import { MarxanRunService } from './marxan-run/marxan-run.service'; -import { MarxanRunController } from './marxan-run/marxan-run.controller'; import { OutputFilesModule } from './output-files/output-files.module'; import { ZipFilesSerializer } from './dto/zip-files.serializer'; import { BoundDatModule } from './input-files/bound.dat.module'; +import { MarxanRunModule } from './marxan-run'; @Module({ imports: [ @@ -77,13 +61,9 @@ import { BoundDatModule } from './input-files/bound.dat.module'; BoundDatModule, PlanningUnitsProtectionLevelModule, OutputFilesModule, - QueueModule.register(), - ApiEventsModule, + MarxanRunModule, ], providers: [ - runQueueProvider, - runQueueEventsProvider, - RunService, ScenariosService, ScenariosCrudService, ProxyService, @@ -93,21 +73,9 @@ import { BoundDatModule } from './input-files/bound.dat.module'; SolutionResultCrudService, ScenarioSolutionSerializer, MarxanInput, - InputParameterFileProvider, - MarxanRunService, ZipFilesSerializer, - { - provide: ioSettingsToken, - useFactory: () => { - const config = AppConfig.get( - 'marxan.inputFiles.inputDat.ioSettings', - ); - assertDefined(config); - return config; - }, - }, ], - controllers: [ScenariosController, MarxanRunController], + controllers: [ScenariosController], exports: [ScenariosCrudService, ScenariosService], }) export class ScenariosModule {} diff --git a/api/apps/api/src/modules/scenarios/scenarios.service.ts b/api/apps/api/src/modules/scenarios/scenarios.service.ts index f08d7fa8bf..3ab557652d 100644 --- a/api/apps/api/src/modules/scenarios/scenarios.service.ts +++ b/api/apps/api/src/modules/scenarios/scenarios.service.ts @@ -28,12 +28,11 @@ import { UpdateScenarioDTO } from './dto/update.scenario.dto'; import { UpdateScenarioPlanningUnitLockStatusDto } from './dto/update-scenario-planning-unit-lock-status.dto'; import { SolutionResultCrudService } from './solutions-result/solution-result-crud.service'; import { CostSurfaceViewService } from './cost-surface-readmodel/cost-surface-view.service'; -import { InputParameterFileProvider } from './input-parameter-file.provider'; import { SpecDatService } from './input-files/spec.dat.service'; import { PuvsprDatService } from './input-files/puvspr.dat.service'; import { OutputFilesService } from './output-files/output-files.service'; import { BoundDatService } from './input-files/bound.dat.service'; -import { notFound, RunService } from './run.service'; +import { notFound, RunService, InputParameterFileProvider } from './marxan-run'; @Injectable() export class ScenariosService { @@ -178,12 +177,13 @@ export class ScenariosService { await this.assertScenario(scenarioId); const result = await this.runService.cancel(scenarioId); if (isLeft(result)) { - const mapping: Record never> = { - [notFound]: () => { + switch (result.left) { + case notFound: throw new NotFoundException(); - }, - }; - mapping[result.left](); + default: + const _check: never = result.left; + throw new InternalServerErrorException(); + } } } diff --git a/api/apps/api/test/jest-e2e.json b/api/apps/api/test/jest-e2e.json index 0aa95dd2e3..d07fe97473 100644 --- a/api/apps/api/test/jest-e2e.json +++ b/api/apps/api/test/jest-e2e.json @@ -27,6 +27,8 @@ "@marxan/planning-area-repository/(.*)": "/../../../libs/planning-area-repository/src/$1", "@marxan/planning-area-repository": "/../../../libs/planning-area-repository/src", "@marxan/scenario-puvspr/(.*)": "/../../../libs/scenario-puvspr/src/$1", - "@marxan/scenario-puvspr": "/../../../libs/scenario-puvspr/src" + "@marxan/scenario-puvspr": "/../../../libs/scenario-puvspr/src", + "@marxan/scenario-run-queue/(.*)": "/../../../libs/scenario-run-queue/src/$1", + "@marxan/scenario-run-queue": "/../../../libs/scenario-run-queue/src" } } diff --git a/api/apps/geoprocessing/src/marxan-sandboxed-runner/marxan-sandbox-runner.service.ts b/api/apps/geoprocessing/src/marxan-sandboxed-runner/marxan-sandbox-runner.service.ts index 271ae21121..a19258f978 100644 --- a/api/apps/geoprocessing/src/marxan-sandboxed-runner/marxan-sandbox-runner.service.ts +++ b/api/apps/geoprocessing/src/marxan-sandboxed-runner/marxan-sandbox-runner.service.ts @@ -10,6 +10,8 @@ import { Cancellable } from './ports/cancellable'; import { Assets, InputFilesFs } from './adapters/scenario-data/input-files-fs'; import { SolutionsOutputService } from './adapters/solutions-output/solutions-output.service'; +export { Assets }; + @Injectable() export class MarxanSandboxRunnerService { readonly #controllers: Record = {}; diff --git a/api/apps/geoprocessing/test/jest-e2e.json b/api/apps/geoprocessing/test/jest-e2e.json index fe8b0d8f72..1e71fd1d6e 100644 --- a/api/apps/geoprocessing/test/jest-e2e.json +++ b/api/apps/geoprocessing/test/jest-e2e.json @@ -27,6 +27,8 @@ "@marxan/marxan-output/(.*)": "/../../libs/marxan-output/src/$1", "@marxan/marxan-output": "/../../libs/marxan-output/src", "@marxan/planning-area-repository/(.*)": "/../../libs/planning-area-repository/src/$1", - "@marxan/planning-area-repository": "/../../libs/planning-area-repository/src" + "@marxan/planning-area-repository": "/../../libs/planning-area-repository/src", + "@marxan/scenario-run-queue/(.*)": "/../../libs/scenario-run-queue/src/$1", + "@marxan/scenario-run-queue": "/../../libs/scenario-run-queue/src" } } diff --git a/api/libs/scenario-run-queue/src/index.ts b/api/libs/scenario-run-queue/src/index.ts new file mode 100644 index 0000000000..96a0699df5 --- /dev/null +++ b/api/libs/scenario-run-queue/src/index.ts @@ -0,0 +1,12 @@ +export const queueName = 'scenario-run-queue'; +export type Assets = { + url: string; + relativeDestination: string; +}[]; +export type JobData = { + scenarioId: string; + assets: Assets; +}; +export type ProgressData = { + canceled: boolean; +}; diff --git a/api/libs/scenario-run-queue/tsconfig.lib.json b/api/libs/scenario-run-queue/tsconfig.lib.json new file mode 100644 index 0000000000..46a8629b17 --- /dev/null +++ b/api/libs/scenario-run-queue/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/scenario-run-queue" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/api/nest-cli.json b/api/nest-cli.json index f9ed348d6a..97c36b1898 100644 --- a/api/nest-cli.json +++ b/api/nest-cli.json @@ -115,6 +115,15 @@ "compilerOptions": { "tsConfigPath": "libs/scenario-puvspr/tsconfig.lib.json" } + }, + "scenario-run-queue": { + "type": "library", + "root": "libs/scenario-run-queue", + "entryFile": "index", + "sourceRoot": "libs/scenario-run-queue/src", + "compilerOptions": { + "tsConfigPath": "libs/scenario-run-queue/tsconfig.lib.json" + } } } -} \ No newline at end of file +} diff --git a/api/package.json b/api/package.json index cee7a4af87..e5e7617350 100644 --- a/api/package.json +++ b/api/package.json @@ -180,7 +180,9 @@ "@marxan/planning-area-repository/(.*)": "/libs/planning-area-repository/src/$1", "@marxan/planning-area-repository": "/libs/planning-area-repository/src", "@marxan/scenario-puvspr/(.*)": "/libs/scenario-puvspr/src/$1", - "@marxan/scenario-puvspr": "/libs/scenario-puvspr/src" + "@marxan/scenario-puvspr": "/libs/scenario-puvspr/src", + "@marxan/scenario-run-queue/(.*)": "/libs/scenario-run-queue/src/$1", + "@marxan/scenario-run-queue": "/libs/scenario-run-queue/src" } } -} \ No newline at end of file +} diff --git a/api/tsconfig.json b/api/tsconfig.json index 4d20b185b1..26533eacb8 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -78,6 +78,12 @@ ], "@marxan/scenario-puvspr/*": [ "libs/scenario-puvspr/src/*" + ], + "@marxan/scenario-run-queue": [ + "libs/scenario-run-queue/src" + ], + "@marxan/scenario-run-queue/*": [ + "libs/scenario-run-queue/src/*" ] } }, @@ -85,4 +91,4 @@ "node_modules", "dist" ] -} \ No newline at end of file +}