Skip to content

Commit b2307f3

Browse files
authored
chore: add availability status to the overviews (#3556)
* chore: add availability status to the overviews fixes RHIDP-14338 assisted by cursor * squash: re-gen api * squash: add a changeset * squash: tsc error fix * squash: api-reports
1 parent b57a430 commit b2307f3

19 files changed

Lines changed: 806 additions & 386 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-orchestrator-backend': patch
3+
'@red-hat-developer-hub/backstage-plugin-orchestrator-common': patch
4+
---
5+
6+
The updated overview of a workflow will have the availability property if the isAvailable prop is false

workspaces/orchestrator/plugins/orchestrator-backend/src/service/OrchestratorService.test.ts

Lines changed: 127 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ const createInstancesMock = (size: number): ProcessInstance[] => {
6363
const instanceId = createInstanceIdMock(1);
6464
const definitionId = createDefinitionIdMock(1);
6565
const workflowInfo = createWorkflowInfoMock(1);
66-
const workflowOverview = createWorkflowOverviewMock(1);
6766
const workflowOverviews = createWorkflowOverviewsMock(3);
6867
const instance = createInstanceMock(1);
6968
const instances = createInstancesMock(3);
@@ -135,8 +134,21 @@ describe('OrchestratorService', () => {
135134
});
136135

137136
describe('fetchWorkflowOverviews', () => {
137+
const availabilityResponse = {
138+
isAvailable: false,
139+
statusCode: 503,
140+
urlToFetch: `${serviceUrl}/management/processes/${definitionId}`,
141+
reason: 'Service Unavailable',
142+
};
143+
138144
beforeEach(() => {
139145
jest.clearAllMocks();
146+
sonataFlowServiceMock.pingWorkflowService = jest
147+
.fn()
148+
.mockResolvedValue(availabilityResponse);
149+
dataIndexServiceMock.fetchWorkflowServiceUrls = jest
150+
.fn()
151+
.mockResolvedValue({ [definitionId]: serviceUrl });
140152
});
141153

142154
it('should throw error when data index returns error', async () => {
@@ -161,6 +173,27 @@ describe('OrchestratorService', () => {
161173

162174
expect(result).toHaveLength(workflowOverviews.length);
163175
expect(sonataFlowServiceMock.fetchWorkflowOverviews).toHaveBeenCalled();
176+
expect(sonataFlowServiceMock.pingWorkflowService).not.toHaveBeenCalled();
177+
});
178+
179+
it('pings unavailable workflows and sets availability on each overview', async () => {
180+
sonataFlowServiceMock.fetchWorkflowOverviews = jest
181+
.fn()
182+
.mockResolvedValue([createWorkflowOverviewMock(1)]);
183+
workflowCacheServiceMock.isAvailable = jest.fn().mockReturnValue(false);
184+
185+
const result = await orchestratorService.fetchWorkflowOverviews({});
186+
187+
expect(result).toHaveLength(1);
188+
expect(result?.[0].isAvailable).toBe(false);
189+
expect(result?.[0].availability).toEqual(availabilityResponse);
190+
expect(
191+
dataIndexServiceMock.fetchWorkflowServiceUrls,
192+
).toHaveBeenCalledTimes(1);
193+
expect(sonataFlowServiceMock.pingWorkflowService).toHaveBeenCalledWith({
194+
definitionId,
195+
serviceUrl,
196+
});
164197
});
165198
});
166199

@@ -286,21 +319,111 @@ describe('OrchestratorService', () => {
286319
});
287320

288321
describe('fetchWorkflowOverview', () => {
322+
const availabilityResponse = {
323+
isAvailable: false,
324+
statusCode: 503,
325+
urlToFetch: `${serviceUrl}/management/processes/${definitionId}`,
326+
reason: 'Service Unavailable',
327+
};
328+
289329
beforeEach(() => {
290330
jest.clearAllMocks();
331+
sonataFlowServiceMock.fetchWorkflowOverview = jest
332+
.fn()
333+
.mockResolvedValue(createWorkflowOverviewMock(1));
334+
sonataFlowServiceMock.pingWorkflowService = jest
335+
.fn()
336+
.mockResolvedValue(availabilityResponse);
337+
dataIndexServiceMock.fetchWorkflowServiceUrls = jest
338+
.fn()
339+
.mockResolvedValue({ [definitionId]: serviceUrl });
291340
});
292341

293-
it('should execute the operation', async () => {
342+
it('sets isAvailable from cache when workflow is available', async () => {
294343
workflowCacheServiceMock.isAvailable = jest.fn().mockReturnValue(true);
344+
345+
const result = await orchestratorService.fetchWorkflowOverview({
346+
definitionId,
347+
});
348+
349+
expect(workflowCacheServiceMock.isAvailable).toHaveBeenCalledWith(
350+
definitionId,
351+
);
352+
expect(result?.isAvailable).toBe(true);
353+
expect(result?.availability).toBeUndefined();
354+
expect(sonataFlowServiceMock.pingWorkflowService).not.toHaveBeenCalled();
355+
});
356+
357+
it('pings workflow service and sets availability when workflow is unavailable', async () => {
358+
workflowCacheServiceMock.isAvailable = jest.fn().mockReturnValue(false);
359+
360+
const result = await orchestratorService.fetchWorkflowOverview({
361+
definitionId,
362+
});
363+
364+
expect(result?.isAvailable).toBe(false);
365+
expect(result?.availability).toEqual(availabilityResponse);
366+
expect(dataIndexServiceMock.fetchWorkflowServiceUrls).toHaveBeenCalled();
367+
expect(sonataFlowServiceMock.pingWorkflowService).toHaveBeenCalledWith({
368+
definitionId,
369+
serviceUrl,
370+
});
371+
});
372+
373+
it('returns undefined without setting availability when overview is not found', async () => {
374+
workflowCacheServiceMock.isAvailable = jest.fn().mockReturnValue(false);
295375
sonataFlowServiceMock.fetchWorkflowOverview = jest
296376
.fn()
297-
.mockResolvedValue(workflowOverview);
377+
.mockResolvedValue(undefined);
298378

299379
const result = await orchestratorService.fetchWorkflowOverview({
300380
definitionId,
301381
});
302382

303-
expect(result).toBeDefined();
383+
expect(result).toBeUndefined();
384+
expect(sonataFlowServiceMock.pingWorkflowService).not.toHaveBeenCalled();
385+
});
386+
});
387+
388+
describe('pingWorkflowService', () => {
389+
beforeEach(() => {
390+
jest.clearAllMocks();
391+
});
392+
393+
it('returns true when the workflow service is available', async () => {
394+
sonataFlowServiceMock.pingWorkflowService = jest.fn().mockResolvedValue({
395+
isAvailable: true,
396+
statusCode: 200,
397+
urlToFetch: `${serviceUrl}/management/processes/${definitionId}`,
398+
reason: 'OK',
399+
});
400+
401+
const result = await orchestratorService.pingWorkflowService({
402+
definitionId,
403+
serviceUrl,
404+
});
405+
406+
expect(result).toBe(true);
407+
expect(sonataFlowServiceMock.pingWorkflowService).toHaveBeenCalledWith({
408+
definitionId,
409+
serviceUrl,
410+
});
411+
});
412+
413+
it('returns false when the workflow service is unavailable', async () => {
414+
sonataFlowServiceMock.pingWorkflowService = jest.fn().mockResolvedValue({
415+
isAvailable: false,
416+
statusCode: 503,
417+
urlToFetch: `${serviceUrl}/management/processes/${definitionId}`,
418+
reason: 'Service Unavailable',
419+
});
420+
421+
const result = await orchestratorService.pingWorkflowService({
422+
definitionId,
423+
serviceUrl,
424+
});
425+
426+
expect(result).toBe(false);
304427
});
305428
});
306429

workspaces/orchestrator/plugins/orchestrator-backend/src/service/OrchestratorService.ts

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,28 @@ export class OrchestratorService {
134134
targetEntity: args.targetEntity,
135135
});
136136

137-
return overviews?.map(overview => {
138-
const updatedOverview = overview;
139-
updatedOverview.isAvailable = this.workflowCacheService.isAvailable(
140-
updatedOverview.workflowId,
141-
);
142-
return updatedOverview;
143-
});
137+
if (!overviews?.length) {
138+
return overviews;
139+
}
140+
141+
const serviceUrls = await this.dataIndexService.fetchWorkflowServiceUrls();
142+
143+
return Promise.all(
144+
overviews.map(async overview => {
145+
const updatedOverview = overview;
146+
updatedOverview.isAvailable = this.workflowCacheService.isAvailable(
147+
updatedOverview.workflowId,
148+
);
149+
if (!updatedOverview.isAvailable) {
150+
updatedOverview.availability =
151+
await this.sonataFlowService.pingWorkflowService({
152+
definitionId: updatedOverview.workflowId,
153+
serviceUrl: serviceUrls[updatedOverview.workflowId],
154+
});
155+
}
156+
return updatedOverview;
157+
}),
158+
);
144159
}
145160

146161
public async executeWorkflowAsCloudEvent(args: {
@@ -183,7 +198,18 @@ export class OrchestratorService {
183198
this.workflowCacheService.isAvailable(definitionId);
184199
const overview =
185200
await this.sonataFlowService.fetchWorkflowOverview(definitionId);
186-
if (overview) overview.isAvailable = isWorkflowAvailable; // workflow overview is avaiable but the workflow itself is not
201+
if (overview) {
202+
overview.isAvailable = isWorkflowAvailable; // workflow overview is avaiable but the workflow itself is not
203+
if (!isWorkflowAvailable) {
204+
overview.availability =
205+
await this.sonataFlowService.pingWorkflowService({
206+
definitionId,
207+
serviceUrl: (
208+
await this.dataIndexService.fetchWorkflowServiceUrls()
209+
)[definitionId],
210+
});
211+
}
212+
}
187213
return overview;
188214
}
189215

@@ -211,6 +237,6 @@ export class OrchestratorService {
211237
definitionId,
212238
serviceUrl,
213239
});
214-
return isServiceUp;
240+
return isServiceUp.isAvailable;
215241
}
216242
}

workspaces/orchestrator/plugins/orchestrator-backend/src/service/SonataFlowService.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,97 @@ describe('SonataFlowService', () => {
667667
});
668668
});
669669

670+
describe('pingWorkflowService', () => {
671+
const urlToFetch = `${serviceUrl}/management/processes/${definitionId}`;
672+
673+
beforeEach(() => {
674+
jest.clearAllMocks();
675+
});
676+
677+
it('returns available when the management endpoint responds ok', async () => {
678+
setupTest({
679+
ok: true,
680+
status: 200,
681+
statusText: 'OK',
682+
json: {},
683+
});
684+
685+
const result = await sonataFlowService.pingWorkflowService({
686+
definitionId,
687+
serviceUrl,
688+
});
689+
690+
expect(fetch).toHaveBeenCalledWith(urlToFetch);
691+
expect(result).toEqual({
692+
isAvailable: true,
693+
statusCode: 200,
694+
urlToFetch,
695+
reason: 'OK',
696+
});
697+
});
698+
699+
it('returns unavailable with status details when the management endpoint fails', async () => {
700+
setupTest({
701+
ok: false,
702+
status: 503,
703+
statusText: 'Service Unavailable',
704+
json: {},
705+
});
706+
707+
const result = await sonataFlowService.pingWorkflowService({
708+
definitionId,
709+
serviceUrl,
710+
});
711+
712+
expect(result).toEqual({
713+
isAvailable: false,
714+
statusCode: 503,
715+
urlToFetch,
716+
reason: 'Service Unavailable',
717+
});
718+
});
719+
720+
it('returns unavailable with a default reason when status text is missing', async () => {
721+
setupTest({
722+
ok: false,
723+
status: 500,
724+
json: {},
725+
});
726+
727+
const result = await sonataFlowService.pingWorkflowService({
728+
definitionId,
729+
serviceUrl,
730+
});
731+
732+
expect(result).toEqual({
733+
isAvailable: false,
734+
statusCode: 500,
735+
urlToFetch,
736+
reason: 'Unknown reason',
737+
});
738+
});
739+
740+
it('returns unavailable when fetch throws an error', async () => {
741+
const errorMessage = 'Network Error';
742+
global.fetch = jest.fn().mockRejectedValue(new Error(errorMessage));
743+
744+
const result = await sonataFlowService.pingWorkflowService({
745+
definitionId,
746+
serviceUrl,
747+
});
748+
749+
expect(result).toEqual({
750+
isAvailable: false,
751+
statusCode: 500,
752+
urlToFetch,
753+
reason: `Failed to fetch from ${urlToFetch}: ${errorMessage}`,
754+
});
755+
expect(loggerMock.error).toHaveBeenCalledWith(
756+
`Failed to fetch from ${urlToFetch}: ${errorMessage}`,
757+
);
758+
});
759+
});
760+
670761
describe('fetchWorkflowOverviews', () => {
671762
const NOW = new Date('2024-06-01T12:00:00Z');
672763
const WITHIN_30_DAYS = '2024-05-15T12:00:00Z';

workspaces/orchestrator/plugins/orchestrator-backend/src/service/SonataFlowService.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
fromWorkflowSource,
2828
ProcessInstanceStateValues,
2929
ProcessInstanceVariables,
30+
WorkflowAvailabilityResponse,
3031
WorkflowDefinition,
3132
WorkflowExecutionResponse,
3233
WorkflowInfo,
@@ -511,7 +512,7 @@ export class SonataFlowService {
511512
public async pingWorkflowService(args: {
512513
definitionId: string;
513514
serviceUrl: string;
514-
}): Promise<boolean> {
515+
}): Promise<WorkflowAvailabilityResponse> {
515516
const urlToFetch = `${args.serviceUrl}/management/processes/${args.definitionId}`;
516517
let response: Response | undefined;
517518
try {
@@ -520,9 +521,19 @@ export class SonataFlowService {
520521
this.logger.error(
521522
`Failed to fetch from ${urlToFetch}: ${(error as Error).message}`,
522523
);
523-
return false;
524+
return {
525+
isAvailable: false,
526+
statusCode: 500,
527+
urlToFetch,
528+
reason: `Failed to fetch from ${urlToFetch}: ${(error as Error).message}`,
529+
};
524530
}
525-
return response.ok;
531+
return {
532+
isAvailable: response.ok,
533+
statusCode: response.status || 500,
534+
urlToFetch,
535+
reason: response.statusText || 'Unknown reason',
536+
};
526537
}
527538

528539
private async handleWorkflowServiceResponse(

0 commit comments

Comments
 (0)