Общение между двумя и более инстансами ECOS’а через команды и события
Требования
Инстансы ECOS должны быть самодостаточны. Т.е. в каждом ECOS должен быть развернут полный список необходимых микросервисов для полноценной работы.
Микросервис интеграции в каждом контуре ECOS должен иметь доступ по сети к RabbitMQ и Zookeeper удаленного ECOS.
В каждом контуре ECOS должен быть микросервис интеграции версии 1.15.0+ (пока SNAPSHOT)
Примечание
доступ к Zookeeper нужен для работы событий, но они пока в TODO, а для работы команд достаточно доступа к RabbitMQ.
Настройка
Схема подключения двух инстансов ECOS’а:
Подключение пунктирными линиями настраиваются в central-config для микросервиса интеграции (в дальнейшем подключение к Zookeeper будет в connections под ключом “zookeeper”):
# Пример конфигурации для main-ecos. Для second-ecos изменится this-app-name и возможно host/user/pass у rabbitmq
ecos-integrations:
extecos:
- this-app-name: main-ecos
connections:
rabbitmq:
host: localhost
username: admin
password: admin
commands:
apps:
- alfresco
extecos - массив из подключений к различным контурам ECOS (поддерживается неограниченное кол-во подключений).
extecos[].this-app-name - имя текущего контура экос. Будет использоваться при отправке команд и событий из внешней системы в текущую
extecos[].connections - раздел с подключениями к RabbitMQ и Zookeeper
extecos[].commands - настройка команд;
extecos[].commands.apps - локальные приложения, которым будет возможность отправлять команды из внешней системы.
Регистрация исполнителя команд
Инжектим CommandsService и вызываем:
commandsService.addExecutor(new SomeExecutor());
Пример исполнителя команд:
public class SomeExecutor implements CommandExecutor<SomeBody> { // SomeBody - любой DTO тип, который будет передаваться в Body команды. DTO тип должен иметь аннотацию CommandType для определения типа команды
@Nullable
@Override
public Object execute(SomeBody someBody) {
//выполняем необходимые действи
return "OK";
}
}
@Data
@CommandType("some-command-type") // тип команды. С отправляющей стороны задается как builder.setType("some-command-type") или так же через аннотацию на типе тела команды, которое передается как builder.setBody(...)
public class SomeBody {
private String strField = "str-field";
private byte[] bytesField;
}
SomeExecutor - принимающая сторона, а отправляющая сторона будет там где вызовется commandsService.execute (пример в разделе “отправка команд”)
SomeBody класс должен быть описан на отправляющей стороне и на принимающей (Дстаточно чтобы имена полей и типы полей совпадали. Пакеты при этом не важны. Jackson позаботится о приобразовании данных).
Отправка команды во внешнюю систему (из ECOS 1 (second-ecos) в ECOS 0 (main-ecos))
Из java кода
Инжектим CommandsService и вызываем отправку команды:
commandsService.executeSync(builder -> { // вместо executeSync можно вызвать просто execute, чтобы не дожидаться ответа.
builder.setTargetApp("main-ecos/alfresco"); // целевое приложение. Является значением this-app-name из конфигурации целевого контура ECOS + "/" + индентификатор целевого приложения
builder.setType("some-command-type"); // тип события. по нему будет выбран CommandExecutor для выполнения. Вместо данной строки тип можно указать через аннотацию @CommandType
builder.setBody(new SomeBody()); // любой инстанс DTO класса. Преобразуется в байты и обратно с помощью библиотеки Jackson
builder.setTtl(Duration.of(1, ChronoUnit.MINUTES)); //время жизни сообщения в RabbitMQ. Если за это время сообщение никто не обработает, то оно удалится из очередей.
return Unit.INSTANCE;
})
Если предположим, что отправка осуществляется из alfresco (ECOS 1 - second-ecos) в alfresco (ECOS 0 - main-ecos), то ход команды будет следующим:
Тестирование отправки команд
Отправка команд в удаленный ECOS и локальный отличается только аргументом в setTargetApp. Т.о. отлаживать механизм можно без учета нескольких инстансов ECOS.
Отправка команды в локальный RabbitMQ через Java тест (можно размещать в ecos-integrations):
public class CommandsTest {
@Test
public void test() {
// подключаемся к нужному RabbitMQ
RabbitMqConnProps props = new RabbitMqConnProps();
props.setUsername("admin");
props.setPassword("admin");
props.setHost("localhost");
RabbitMqConnFactory factory = new RabbitMqConnFactory();
RabbitMqConn conn = factory.createConnection(props, 0);
conn.waitUntilReady(5000);
CommandsServiceFactory commFactory = new CommandsServiceFactory() {
@NotNull
@Override
protected CommandsProperties createProperties() {
CommandsProperties props = new CommandsProperties();
props.setAppName("alfresco1"); // "представляемся" в системе как приложение с именем "alfresco1"
props.setAppInstanceId("alfresco1-123"); // идентификатор инстанса приложения
props.setListenBroadcast(false); // указываем, что широковещательные команды нам исполнять не нужно
return props;
}
@NotNull
@Override
protected RemoteCommandsService createRemoteCommandsService() {
return new RabbitCommandsService(this, conn);
}
};
commFactory.getRemoteCommandsService();
CommandsService commandsService = commFactory.getCommandsService();
System.out.println(commandsService.executeSync(builder -> { // выполняем команду синхронно и выводим результат в консоль
builder.setTargetApp("alfresco"); // отправляем команду в alfresco
builder.setType("some-command-type"); // тип команды
builder.setBody(new SomeBody()); // тело команды
builder.setTtl(Duration.of(1, ChronoUnit.MINUTES));
return Unit.INSTANCE;
}));
conn.close();
}
@Data
@CommandType("some-command-type")
public static class SomeBody {
private String strField = "str-field";
private byte[] bytesField;
}
}
Локальное тестирование отправки команд на удаленный инстанс (имеет смысл после отладки через обычную отправку команд):
Добавляем настройку удаленного контура ECOS как описано в разделе “Настройка”. В качестве целевого RabbitMQ выбираем localhost. Т.о. можно локально тестировать работу с удаленными инстансами подняв только один инстанс RabbitMQ. Конфликтов при этом не возникнет.
Немного меняем аргумент в методе setTargetApp при отправке команды в тесте:
... здесь все аналогично предыдущему блоку кода, который описывает класс CommandsTest ...
System.out.println(commandsService.executeSync(builder -> {
builder.setTargetApp("main-ecos/alfresco"); // единственное отличие при отправке команд - добавляется идентификатор контура ECOS со слэшем
builder.setType("some-command-type");
builder.setBody(new SomeBody());
builder.setTtl(Duration.of(1, ChronoUnit.MINUTES));
return Unit.INSTANCE;
}));
... здесь все аналогично предыдущему блоку кода, который описывает класс CommandsTest ...
При желании можно подключиться и к реальному удаленному ECOS, но для этого должен быть доступ к RabbitMQ извне. При этом достаточно будет исправить параметры в RabbitMqConnProps
Отправка файлов в командах и событиях
Для отправки файлов в командах и событиях следует использовать поля с типом byte[] (сообщения сжимаются перед отправкой. Т.е. доп. оптимизация не нужна).
Для удобной работы с файлами есть утилитные классы EcosMemFile EcosMemDir и ru.citeck.ecos.commons.utils.ZipUtils, который может легко упаковывать много файлов в один поток байт и обратно.
Пример:
SomeBody body = new SomeBody();
EcosMemDir dir = new EcosMemDir();
dir.createFile("firstFile.txt", "content");
dir.createFile("secondFile.docx", new byte[10]);
body.setBytesField(ZipUtils.writeZipAsBytes(dir));
commandsService.executeSync(builder -> {
builder.setTargetApp("main-ecos/alfresco");
builder.setType("some-command-type");
builder.setBody(body);
builder.setTtl(Duration.of(1, ChronoUnit.MINUTES));
return Unit.INSTANCE;
});