Skip to main content

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

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

Свойства сервиса

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

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

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

Определяется через методы:

  • this.addRequest(requestSchema)
  • this.addEvent(requestSchema)
  • this.setSvcSchema(jsonSchema)

Структуры описаны ниже

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

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

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

Запросы

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

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

При добавлении запроса через this.addRequest({...}) создаётся объект 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 и т.д.) при экспорте через ServiceBridge.

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

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 });

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

class MyService extends ServiceRequire {
    constructor(host) {
      super(host);
      /** @type {Database} */
      this.Database = null //Будет автоматически заполнен после start
      /** @type {Cache} */
      this.Cache = null //Будет автоматически заполнен после start
      this.requirements = ['Database', 'Cache'];   // жёсткие зависимости
    }

    async start(log) {
      await super.start(log)
      //Здесь доступен this.Database и this.Cache
    }

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

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

События

Схема события (EventSchema)

Поле Тип Описание
name string Имя события
description string Описание
structure object Структура передаваемых данных

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

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

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

Таймер (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);
      /** @type {Database} */
      this.Database = null //Будет автоматически заполнен после start
      /** @type {Cache} */
      this.Cache = null //Будет автоматически заполнен после start
      this.requirements = ['Database', 'Cache'];   // жёсткие зависимости
    }

    async start(log) {
      await super.start(log)
      //Здесь доступен this.Database и this.Cache
    }

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

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

Логирование

Каждый сервис получает экземпляр 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

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

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

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

На данный момент - это экспериментальная функциональность