feat_the_progress_of_tasks #5
@@ -11,3 +11,5 @@ registerEnumType(TaskStatuses, {
 | 
			
		||||
  name: 'TaskStatuses',
 | 
			
		||||
  description: '任务状态',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const terminalTaskStatuses = [TaskStatuses.success, TaskStatuses.failed];
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										80
									
								
								src/pipeline-tasks/pipeline-task-flush.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/pipeline-tasks/pipeline-task-flush.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
			
		||||
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { RedisService } from 'nestjs-redis';
 | 
			
		||||
import { PipelineTaskFlushService } from './pipeline-task-flush.service';
 | 
			
		||||
import { PipelineTaskEvent } from './models/pipeline-task-event';
 | 
			
		||||
import { TaskStatuses } from './enums/task-statuses.enum';
 | 
			
		||||
import {
 | 
			
		||||
  EXCHANGE_PIPELINE_TASK_TOPIC,
 | 
			
		||||
  ROUTE_PIPELINE_TASK_DONE,
 | 
			
		||||
} from './pipeline-tasks.constants';
 | 
			
		||||
 | 
			
		||||
describe('PipelineTaskFlushService', () => {
 | 
			
		||||
  let service: PipelineTaskFlushService;
 | 
			
		||||
  let redisService: RedisService;
 | 
			
		||||
  let amqpConnection: AmqpConnection;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    const redisClient = {
 | 
			
		||||
      rpush: jest.fn(() => Promise.resolve()),
 | 
			
		||||
      lrange: jest.fn(() => Promise.resolve()),
 | 
			
		||||
    };
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      providers: [
 | 
			
		||||
        PipelineTaskFlushService,
 | 
			
		||||
        {
 | 
			
		||||
          provide: RedisService,
 | 
			
		||||
          useValue: {
 | 
			
		||||
            getClient() {
 | 
			
		||||
              return redisClient;
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: AmqpConnection,
 | 
			
		||||
          useValue: {
 | 
			
		||||
            request: jest.fn(() => Promise.resolve()),
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    service = module.get<PipelineTaskFlushService>(PipelineTaskFlushService);
 | 
			
		||||
    redisService = module.get<RedisService>(RedisService);
 | 
			
		||||
    amqpConnection = module.get<AmqpConnection>(AmqpConnection);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(service).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('write', () => {
 | 
			
		||||
    it('normal', async () => {
 | 
			
		||||
      const testEvent = new PipelineTaskEvent();
 | 
			
		||||
      testEvent.taskId = 'test';
 | 
			
		||||
      testEvent.status = TaskStatuses.working;
 | 
			
		||||
      const rpush = jest.spyOn(redisService.getClient(), 'rpush');
 | 
			
		||||
      const request = jest.spyOn(amqpConnection, 'request');
 | 
			
		||||
      await service.write(testEvent);
 | 
			
		||||
      expect(rpush).toBeCalledTimes(1);
 | 
			
		||||
      expect(rpush.mock.calls[0][0]).toEqual('p-task:log:test');
 | 
			
		||||
      expect(rpush.mock.calls[0][1]).toEqual(JSON.stringify(testEvent));
 | 
			
		||||
      expect(request).toBeCalledTimes(0);
 | 
			
		||||
    });
 | 
			
		||||
    it('event for which task done', async () => {
 | 
			
		||||
      const testEvent = new PipelineTaskEvent();
 | 
			
		||||
      testEvent.taskId = 'test';
 | 
			
		||||
      testEvent.status = TaskStatuses.success;
 | 
			
		||||
      const rpush = jest.spyOn(redisService.getClient(), 'rpush');
 | 
			
		||||
      const request = jest.spyOn(amqpConnection, 'request');
 | 
			
		||||
      await service.write(testEvent);
 | 
			
		||||
      expect(rpush).toBeCalledTimes(1);
 | 
			
		||||
      expect(request).toBeCalledTimes(1);
 | 
			
		||||
      expect(request.mock.calls[0][0]).toMatchObject({
 | 
			
		||||
        exchange: EXCHANGE_PIPELINE_TASK_TOPIC,
 | 
			
		||||
        routingKey: ROUTE_PIPELINE_TASK_DONE,
 | 
			
		||||
        payload: { taskId: 'test', status: TaskStatuses.success },
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										57
									
								
								src/pipeline-tasks/pipeline-task-flush.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/pipeline-tasks/pipeline-task-flush.service.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
import { AmqpConnection, RabbitSubscribe } from '@golevelup/nestjs-rabbitmq';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { deserialize } from 'class-transformer';
 | 
			
		||||
import { RedisService } from 'nestjs-redis';
 | 
			
		||||
import { isNil } from 'ramda';
 | 
			
		||||
import { getSelfInstanceQueueKey } from '../commons/utils/rabbit-mq';
 | 
			
		||||
import { terminalTaskStatuses } from './enums/task-statuses.enum';
 | 
			
		||||
import { PipelineTaskEvent } from './models/pipeline-task-event';
 | 
			
		||||
import {
 | 
			
		||||
  EXCHANGE_PIPELINE_TASK_TOPIC,
 | 
			
		||||
  ROUTE_PIPELINE_TASK_DONE,
 | 
			
		||||
} from './pipeline-tasks.constants';
 | 
			
		||||
import {
 | 
			
		||||
  EXCHANGE_PIPELINE_TASK_FANOUT,
 | 
			
		||||
  ROUTE_PIPELINE_TASK_LOG,
 | 
			
		||||
  QUEUE_WRITE_PIPELINE_TASK_LOG,
 | 
			
		||||
} from './pipeline-tasks.constants';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class PipelineTaskFlushService {
 | 
			
		||||
  constructor(
 | 
			
		||||
    private readonly redisService: RedisService,
 | 
			
		||||
    private readonly amqpConnection: AmqpConnection,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  @RabbitSubscribe({
 | 
			
		||||
    exchange: EXCHANGE_PIPELINE_TASK_FANOUT,
 | 
			
		||||
    routingKey: ROUTE_PIPELINE_TASK_LOG,
 | 
			
		||||
    queue: getSelfInstanceQueueKey(QUEUE_WRITE_PIPELINE_TASK_LOG),
 | 
			
		||||
    queueOptions: {
 | 
			
		||||
      autoDelete: true,
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
  async write(message: PipelineTaskEvent) {
 | 
			
		||||
    await this.redisService
 | 
			
		||||
      .getClient()
 | 
			
		||||
      .rpush(this.getKey(message.taskId), JSON.stringify(message));
 | 
			
		||||
    if (isNil(message.unit) && terminalTaskStatuses.includes(message.status)) {
 | 
			
		||||
      this.amqpConnection.request({
 | 
			
		||||
        exchange: EXCHANGE_PIPELINE_TASK_TOPIC,
 | 
			
		||||
        routingKey: ROUTE_PIPELINE_TASK_DONE,
 | 
			
		||||
        payload: { taskId: message.taskId, status: message.status },
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async read(taskId: string) {
 | 
			
		||||
    const raw = await this.redisService
 | 
			
		||||
      .getClient()
 | 
			
		||||
      .lrange(this.getKey(taskId), 0, -1);
 | 
			
		||||
    return raw.map((it) => deserialize(PipelineTaskEvent, it));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getKey(taskId: string) {
 | 
			
		||||
    return `p-task:log:${taskId}`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -23,13 +23,17 @@ describe('PipelineTaskRunner', () => {
 | 
			
		||||
    it('normal', async () => {
 | 
			
		||||
      const event = new PipelineTaskEvent();
 | 
			
		||||
      event.taskId = 'test';
 | 
			
		||||
      event.emittedAt = new Date().toISOString() as any;
 | 
			
		||||
      const emittedAt = new Date();
 | 
			
		||||
      event.emittedAt = emittedAt.toISOString() as any;
 | 
			
		||||
      const message$ = logger.getMessage$('test');
 | 
			
		||||
 | 
			
		||||
      let receiveEvent;
 | 
			
		||||
      message$.pipe(take(1)).subscribe((value) => (receiveEvent = value));
 | 
			
		||||
      await logger.handleEvent(event);
 | 
			
		||||
      expect(receiveEvent).toEqual(event);
 | 
			
		||||
      expect(receiveEvent).toMatchObject({
 | 
			
		||||
        ...event,
 | 
			
		||||
        emittedAt,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('no match', async () => {
 | 
			
		||||
      const event = new PipelineTaskEvent();
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq';
 | 
			
		||||
import { Injectable, OnModuleDestroy } from '@nestjs/common';
 | 
			
		||||
import { plainToClass } from 'class-transformer';
 | 
			
		||||
import { Observable, Subject } from 'rxjs';
 | 
			
		||||
import { filter, publish, tap } from 'rxjs/operators';
 | 
			
		||||
import { filter } from 'rxjs/operators';
 | 
			
		||||
import { PipelineTaskEvent } from './models/pipeline-task-event';
 | 
			
		||||
import {
 | 
			
		||||
  EXCHANGE_PIPELINE_TASK_FANOUT,
 | 
			
		||||
@@ -14,20 +14,21 @@ import {
 | 
			
		||||
export class PipelineTaskLogger implements OnModuleDestroy {
 | 
			
		||||
  private readonly messageSubject = new Subject<PipelineTaskEvent>();
 | 
			
		||||
  private readonly message$: Observable<PipelineTaskEvent> = this.messageSubject.pipe();
 | 
			
		||||
 | 
			
		||||
  @RabbitSubscribe({
 | 
			
		||||
    exchange: EXCHANGE_PIPELINE_TASK_FANOUT,
 | 
			
		||||
    routingKey: ROUTE_PIPELINE_TASK_LOG,
 | 
			
		||||
    queue: QUEUE_HANDLE_PIPELINE_TASK_LOG_EVENT,
 | 
			
		||||
    queueOptions: {
 | 
			
		||||
      autoDelete: true,
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
  async handleEvent(message: PipelineTaskEvent) {
 | 
			
		||||
    this.messageSubject.next(plainToClass(PipelineTaskEvent, message));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getMessage$(taskId: string) {
 | 
			
		||||
    return this.message$.pipe(
 | 
			
		||||
      tap((val) => console.log(val)),
 | 
			
		||||
      filter((event) => event.taskId === taskId),
 | 
			
		||||
    );
 | 
			
		||||
    return this.message$.pipe(filter((event) => event.taskId === taskId));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onModuleDestroy() {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,3 +2,5 @@ export const EXCHANGE_PIPELINE_TASK_TOPIC = 'pipeline-task.topic';
 | 
			
		||||
export const EXCHANGE_PIPELINE_TASK_FANOUT = 'pipeline-task.fanout';
 | 
			
		||||
export const ROUTE_PIPELINE_TASK_LOG = 'pipeline-task-log';
 | 
			
		||||
export const QUEUE_HANDLE_PIPELINE_TASK_LOG_EVENT = 'pipeline-task-log';
 | 
			
		||||
export const QUEUE_WRITE_PIPELINE_TASK_LOG = 'write-pipeline-task-log';
 | 
			
		||||
export const ROUTE_PIPELINE_TASK_DONE = 'pipeline-task-done';
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,12 @@ import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
 | 
			
		||||
import { ConfigModule, ConfigService } from '@nestjs/config';
 | 
			
		||||
import { PipelineTaskRunner } from './pipeline-task.runner';
 | 
			
		||||
import { spawn } from 'child_process';
 | 
			
		||||
import { EXCHANGE_PIPELINE_TASK_FANOUT } from './pipeline-tasks.constants';
 | 
			
		||||
import {
 | 
			
		||||
  EXCHANGE_PIPELINE_TASK_FANOUT,
 | 
			
		||||
  EXCHANGE_PIPELINE_TASK_TOPIC,
 | 
			
		||||
} from './pipeline-tasks.constants';
 | 
			
		||||
import { PipelineTaskLogger } from './pipeline-task.logger';
 | 
			
		||||
import { PipelineTaskFlushService } from './pipeline-task-flush.service';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [
 | 
			
		||||
@@ -56,6 +60,14 @@ import { PipelineTaskLogger } from './pipeline-task.logger';
 | 
			
		||||
              autoDelete: true,
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: EXCHANGE_PIPELINE_TASK_TOPIC,
 | 
			
		||||
            type: 'topic',
 | 
			
		||||
            options: {
 | 
			
		||||
              durable: false,
 | 
			
		||||
              autoDelete: true,
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      }),
 | 
			
		||||
      inject: [ConfigService],
 | 
			
		||||
@@ -70,6 +82,7 @@ import { PipelineTaskLogger } from './pipeline-task.logger';
 | 
			
		||||
      provide: 'spawn',
 | 
			
		||||
      useValue: spawn,
 | 
			
		||||
    },
 | 
			
		||||
    PipelineTaskFlushService,
 | 
			
		||||
  ],
 | 
			
		||||
  exports: [PipelineTasksService],
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user