Skip to main content

Структура сервисов Morphcluster

Базовый класс Service

Service — это основа для всех сервисов. Каждый сервис обладает:

  • именем (name) — обычно совпадает с именем класса;
  • схемой (schema) — описывает запросы, события и метаданные;
  • запросами (request) — методы, доступных для вызова извне
  • логгером (Logger) — изолированным экземпляром Logger;
  • конфигурацией (Config) — глобальной конфигурацией приложения;
  • состояниямиstarting, started, private (локальный/приватный), remote (виртуальный).

Схема сервиса

Схема задаётся через объект ServiceSchema и определяет публичный контракт сервиса.

import Service from './service.js';

class MyService extends Service {
    constructor() {
        super();
        this.addRequest({
            name: 'ping',
            description: 'Проверка связи',
            anonymous: true
        });
        this.addEvent({
            name: 'onUpdate',
            description: 'Событие обновления данных'
        });
    }

    async ping(params, workspace, log) {
        return { pong: true };
    }
}

EventSchema — описание события

ПолеТипОписание
namestringИмя события
descriptionstringОписание
structureobjectСтруктура передаваемых данных

Жизненный цикл

[создание] → host.addService(service) → host.startService(service)
                                            → service.start(log)
                                            → service.started = true
                                            → service.stop()
                                            → service.started = false

При возникновении ошибки во время старта хост автоматически попытается перезапустить сервис через 5 секунд.

Важные свойства

Свойство Тип Описание
name string Имя сервиса (по умолчанию имя класса)
started boolean Запущен ли сервис в данный момент
starting boolean Находится ли в процессе запуска
private / local boolean Сервис не публикуется в глобальной системе
remote boolean Виртуальный сервис, не добавляется в глобальную систему
clients ServiceClient[] Клиенты, через которые сервис обращается к другим сервисам
timers Timer[] Периодические задачи
triggers Trigger[] Обработчики событий других сервисов
senders ChannelSender[] Каналы для публикации данных
aggregators ChannelAggregator[] Агрегаторы данных из каналов

С

Запросы

Каждый сервис может предоставлять один или несколько запросов — методов, доступных для вызова извне (другимаи сервисами,

через HTTP-мост, клиентами).
Запрос описывается схемой RequestSchema и реализуется методом сервиса с определённой сигнатурой.

Схема запроса (RequestSchema)

При добавлении запроса через this.addRequest({...}) создаётся через объект ServiceSchemaRequestSchema со следующими попределяет публмичный контракт сервиса.:

import Service from './service.js';

class MyService extends Service {
    constructor() {
        super();
        this.addRequest({
            name: 'ping',
            description: 'Проверка связи',
            anonymous: true
        });
        this.addEvent({
            name: 'onUpdate',
            description: 'Событие обновления данных'
        });
    }

    async ping(params, workspace, log) {
        return { pong: true };
    }
}

RequestSchema — описание запроса

Поле ТипОбязательное Описание
name string ИдаУникальное имя запроса (в рамках сервиса. Должно совпадать с именем метода-обязработельное)чика.
description string ОнетЧеловекочитаемое описание.
request object нетJSON-схема (стандарт JSON Schema) для валидации входящих параметров.
response object нетJSON-схема, описывающая структуру ответа.
anonymous boolean ДнетЕсли true, запрос доступен без аутентификации.
needAdmin boolean ТнетЕсли true, требует прав администратора.
noLogs boolean НнетОтключает автоматическое создание логировать запрос.
http string нетHTTP-метод (еслGET, POST и сервист.д.) публри экуспортется через HTTP)ServiceBridge.

EventSchema

Пример добавления запроса:

this.addRequest({
    name: 'getReport',
    description: 'Возвращает отчёт за период',
    request: {
        type: 'object',
        properties: {
            from: { type: 'string', format: 'date' },
            to:   { type: 'string', format: 'date' }
        },
        required: ['from', 'to']
    },
    response: {
        type: 'object',
        properties: {
            rows: { type: 'array' }
        }
    }
});

Метод-обработчик запроса

Для каждого запроса, объявленного в схеме, в классе сервиса должен быть реализован асинхронный метод с точно таким же именем.

async methodName(params, ws, log) {
    // params – объект с параметрами запроса
    // ws – Устаревший параметр, всегда null
    // log – экземпляр Logger для данного вызова
}
  • Параметры не валидируются автоматически на уровне ядра. Рекомендуется проверять входные данные внутри метода.
  • Если запрос помечен как noLogs: true, всё равно можно писать в log — это безопасно, так как логгер проигнорирует запись.
  • Метод может возвращать любое сериализуемое значение (объект, массив, число, строку и т.д.). Этот результат будет передан обратно вызывающей стороне.
  • Все обработчики должны быть async, даже если внутри нет асинхронных операций.
  • Не забывайте закрывать дочерние логи, созданные внутри метода, или используйте log.wrap() для автоматического закрытия.

Обработка ошибок

ComplexError – специальный класс ошибки, позволяющий передать дополнительную информацию:

  • name – тип ошибки (строка),
  • payload – любые данные,
  • options.showUser – показывать ли сообщение пользователю,
  • options.httpStatus – HTTP-статус (при использовании ServiceBridge),

Если метод выбрасывает ComplexError, он будет проброшен до вызывающего кода с сохранением всех свойств. ServiceBridge автоматически добавляет в ошибку logId текущего лога.

Любое другое исключение (Error) будет преобразовано в ComplexError с именем класса ошибки.

throw new ComplexError('Отчёт не найден', 'ReportNotFound', { from, to }, { showUser: true });

События

йсподдержкой:

По

Реале

ТипОпзацисание
namestringИмя события
descriptionstringО
  • подписки/отписки (on, off),
  • вложенных событий (addSubEvent, removeSubEvent),
  • автоматического вызова onSubscribe/onUnsubscribe при появление
  • structureobjectСтруктураи первого/последнего слушаватемыхля. данных

    Таймеры и триггеры

    Таймер (Timer) — периодическая задача.

    this.timers.push(new Timer('cleanup', 60000, async (log) => {
        // очистка каждые 60 секунд
    }));
    

    Триггер (Trigger) — реакция на событие другого сервиса.

    this.triggers.push(new Trigger('onDataChanged', async (log, data) => {
        // обработать событие
    }));
    // во время старта нужно подключить триггер к событию
    trigger.connect(targetService.getEvent('onDataChanged'));
    

    Оба компонента автоматически инициализируются при старте сервиса (им присваиваются serviceName и logger).

    Каналы (ChannelSender / ChannelAggregator)

    • ChannelSender — позволяет сервису публиковать данные в именованный канал.
    • ChannelAggregator — собирает данные от нескольких сервисов в одном канале, вызывая событие при изменении.

    Пример использования:

    this.senders.push(new ChannelSender('metrics'));
    // где-то в коде
    this.senders[0].data = { cpu: 50 };
    
    this.aggregators.push(new ChannelAggregator('metrics'));
    this.aggregators[0].onDataChanged.on((ws, allData) => {
        // allData — массив { serviceName, data }
    });
    

    Объявление зависимостей

    class MyService extends ServiceRequire {
        constructor(host) {
            super(host);
            this.requirements = ['Database', 'Cache'];   // жёсткие зависимости
            this.optionalServices = ['Metrics'];         // опциональные, не блокируют старт
        }
    }
    

    3.2. Процесс старта

    1. Запускается start(log) родительского класса.
    2. Для каждого имени в optionalServices вызывается host.waitService(optName), но без ожидания.
    3. Для requirements формируются Promise на waitRequire(svcName, log), которые резолвятся после получения сервиса.
    4. Все требования загружаются параллельно (Promise.all).
    5. Загруженные сервисы сохраняются в свойства this[svcName].

    Состояние загрузки можно отследить через requirementsStatus().

    ServiceClient — клиент к сервису

    Клиент позволяет сервису обращаться к другому сервису, не заботясь о его физическом расположении (локальный или удалённый).

    Создание клиента

    Клиент создаётся на основе схемы целевого сервиса:

    const client = new ServiceClient({
        name: 'Auth',
        requests: [{ name: 'admin' }],
        events: [{ name: 'onLogin' }]
    });
    this.clients.push(client);
    

    Использование

    • Вызов запроса — через client.requestHandlers[i](log, params). Обычно клиент автоматически связывается с глобальным сервисом (см. GlobalServices).
    • Подписка на событиеclient.eventHandlers[i].on(callback).

    Метод waitConnect() возвращает Promise, который разрешится после подключения клиента к реальному сервису.

    Логирование

    Каждый сервис получает экземпляр Logger с предустановленными service и host. Логгер поддерживает:

    • Дочерние логи (createWriter, attachWriter) для операций внутри сервиса.
    • Автоматический захват ошибок (writeException, wrap, createWrapped).
    • Бэкенды: консоль, файл, воркер (через LoggerBackendWorker).

    Пример использования внутри сервиса:

    async myMethod(params) {
        const log = this.Logger.createWriter('MyMethod', { params });
        try {
            // работа
            log.close();
        } catch (e) {
            log.writeException(e);
        }
    }
    

    Пример создания собственного сервисаConfig

    import { ServiceRequire, Timer } from '@morphcluster/core';
    
    export default class ReportService extends ServiceRequire {
        constructor(host) {
            super(host);
            this.requirements = ['Database'];
            this.optionalServices = ['Notification'];
    
            this.addRequest({
                name: 'generate',
                description: 'Сгенерировать отчёт',
                request: {
                    type: 'object',
                    properties: {
                        from: { type: 'string' },
                        to: { type: 'string' }
                    }
                }
            });
    
            this.timers.push(new Timer('dailyReport', 86400000, this.dailyReport.bind(this)));
        }
    
        async start(log) {
            await super.start(log);
            // здесь Database уже доступен через this.Database
        }
    
        async generate(params, workspace, log) {
            const data = await this.Database.query(/* ... */);
            return { report: data };
        }
    
        async dailyReport(log) {
            log.write('Starting daily report');
            // логика отчёта
        }
    }
    

    Вспомогательные компоненты

    ComplexError

    Обёртка над Error с дополнительными полями:

    • name — тип ошибки,
    • payload — произвольные данные,
    • options — showUser, httpStatus, logId.

    Удобно выбрасывать в методах сервиса.

    Config

    Глобальная конфигурация, загружаемая при старте приложения. Сервис получает её через this.Config.

    Event

    Реализация событий с поддержкой:

    • подписки/отписки (on, off),
    • вложенных событий (addSubEvent, removeSubEvent),
    • автоматического вызова onSubscribe/onUnsubscribe при появлении первого/последнего слушателя.

    Это позволяет легко комбинировать сервисы, запускать их в разных процессах и обеспечивать надёжность за счёт автоматического перезапуска и отслеживания состояния.