meet.js Gdańsk #22
Gdańsk, 20.01.2020
Co-founder / Chief Technology Officer
Trener
Full-stack JavaScript Developer
Klient prosi o przygotowanie początkowej wersji aplikacji w celu przetestowania zakupionego projektu graficznego.
import { Controller, Get } from '@nestjs/common';
import { Product } from '@angular-store/api-interfaces';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('products')
async getData(): Promise<Product[]> {
return this.appService.getProducts()
}
}
import { Injectable } from '@nestjs/common';
import { Product } from '@angular-store/api-interfaces';
@Injectable()
export class AppService {
getProducts(): Promise<Product[]> {
return Promise.resolve([
{
name: 'Stickers bag',
price: 4.99,
description: 'A collection of Angular stickers.'
}
]);
}
}
W trakcie kolejnej sesji planowania klient prosi Mariusza o przygotowanie strony z listą wszystkich zrealizowanych zamówień w danym miesiącu.
const orders: Order[] = [
{
id: 1,
orderDate: new Date(Date.parse('2019-07-11T16:15:00')),
userEmail: 'customer1@dev.null',
items: [
{ productId: 1, price: 4.00, quantity: 1 },
{ productId: 3, price: 3.99, quantity: 2 }
]
}
];
getOrders(): Promise<OrderWithTotal[]> {
const orders: Order[] = [...];
return Promise.resolve(orders.map(order => {
const orderWithTotal: OrderWithTotal = { ...order, total: 0 };
orderWithTotal.total = order.items.reduce((tot, item) => {
return tot + item.quantity * item.price;
}, 0);
return orderWithTotal;
}));
}
Po pewnym czasie funkcjonowania aplikacji na środowisku produkcyjnym klient zaobserwował, że wraz ze zwiększającą się ilością zamówień działanie strony z zamówieniami staje się coraz bardziej kłopotliwe.
The HyperText Transfer Protocol (HTTP) 504 Gateway Timeout server error response code indicates that the server, while acting as a gateway or proxy, did not get a response in time from the upstream server that it needed in order to complete the request.
źródło: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504
By default, the Server's timeout value is 2 minutes, and sockets are destroyed automatically if they time out. However, if you assign a callback to the Server's 'timeout' event, then you are responsible for handling socket timeouts.
źródło: https://nodejs.org/dist/latest-v6.x/docs/api/http.html#http_server_settimeout_msecs_callback
async function bootstrap() {
const app = await NestFactory
.create<NestExpressApplication>(AppModule);
app.getHttpServer().setTimeout(180000);
// ...
}
W trakcie kolejnej sesji planowania wywiązała się żywa dyskusja między Mariuszem a Klientem na temat stabilności strony zamówień. Ustalono wówczas, że zamiast generować raport w czasie rzeczywistym będzie on tworzony w tle.
@Post('orders/report')
async generateReports(@Body('month') month: number,
@Body('year') year: number) {
const report = await this.appService
.getOrderReport(year, month);
if (report === null) {
setTimeout(() => {
this.appService
.generateOrderReport(year, month);
}, 0);
}
return {};
}
import { Injectable } from '@nestjs/common';
import * as Bull from 'bull';
import { getRedisConfig } from './config.helpers';
@Injectable()
export class OrderReportService {
private queue: Bull.Queue;
private readonly queueName = 'order_reports';
constructor() {
this.queue = new Bull(this.queueName, {
redis: getRedisConfig()
});
}
}
this.queue.process('generate_report',
async (job: Bull.Job, done: Bull.DoneCallback) => {
setTimeout(() => {
console.log(`Report generated!`);
console.log(JSON.stringify(job.data));
done();
}, 5000);
}
);
async generate(year: number, month: number) {
await this.queue.add('generate_report', {
year,
month
});
}
Od wprowadzenia systemu kolejkowania w aplikacji pojawiło się 6 nowych typów raportów i przyszedł czas aby poprawić wydajność systemu ich generowania (stabilność na tym etapie jest zadawalająca).
źródło: https://optimalbits.github.io/bull/
Po kolejnych usprawnieniach w systemie generowania raportów Klient poprosił Mariusza o możliwość generowania wybranego raportu cyklicznie.
this.paymentsQueue = new Bull(this.queueName, {
redis: getRedisConfig()
});
// Queue process logic omitted...
this.paymentsQueue.add({}, {
repeat: { cron: '0 10 * * MON-FRI' }
});
{
"queues": [{
"name": "order_reports",
"hostId": "Order Reports",
"host": "docker.for.mac.localhost",
"port": 6379
}]
}
docker run -p 4567:4567 \
--name bull_arena --rm \
-v ~/arena/config.json
:/opt/arena/src/server/config/index.json \
mixmaxhq/arena:latest
W trakcie kolejnej sesji planowania klient poprosił Mariusza o dodanie do systemu raportowania powiadomień mailowych kiedy dany raport jest już gotowy.
this.queue.on('completed', (async job => {
await this.sendEmailNotification(
job.data.year,
job.data.month
);
}));
.on('error', (error) => {});
.on('waiting', (jobId) => {
// A Job is waiting to be processed
// as soon as a worker is idling.
});
.on('active', (job, jobPromise) => {
// A job has started.
// You can use jobPromise.cancel()
// to abort it.
});
.on('stalled', (job) => { });
.on('progress', (job, progress) => {
// A job's progress was updated!
});
.on('completed', (job, result) =>{
// A job successfully completed with a `result`.
});
.on('failed', (job, err) => {
// A job failed with reason `err`!
});
.on('paused', () => {
// The queue has been paused.
});
.on('resumed', (job) =>{
// The queue has been resumed.
})
.on('cleaned', (jobs, type) => {
// Old jobs have been cleaned
// from the queue. `jobs` is
// an array of cleaned jobs,
// and `type` is the type of
// jobs cleaned.
});
.on('drained', () => {
// Emitted every time the queue has
// processed all the waiting jobs
// (even if there can be some
// delayed jobs not yet processed)
});
.on('removed', (job) => {
// A job successfully removed.
});
// Local event
queue.on('completed', listener):
// Global event - listens to whole queue,
// distributed across Redis listeners
queue.on('global:completed', listener);
const myJob = await myqueue.add({
foo: 'bar'
}, {
priority: 3
});
this.queue.process('generate_report', 5,
async (job: Bull.Job, done: Bull.DoneCallback) => {
setTimeout(() => {
console.log(`Report generated!`);
console.log(JSON.stringify(job.data));
done();
}, 5000);
}
);
Sometimes jobs are more CPU intensive which will could lock the Node event loop for too long and Bull could decide the job has been stalled. To avoid this situation, it is possible to run the process functions in separate Node processes. In this case, the concurrency parameter will decide the maximum number of concurrent processes that are allowed to run.