.. _two_instances: Общение между двумя и более инстансами Citeck через команды и события =========================================================================== .. contents:: :depth: 3 Требования ----------- 1. Инстансы Citeck должны быть самодостаточны. Т.е. в каждом Citeck должен быть развернут полный список необходимых микросервисов для полноценной работы. 2. Микросервис интеграции в каждом контуре Citeck должен иметь доступ по сети к RabbitMQ и Zookeeper удаленного Citeck. 3. В каждом контуре Citeck должен быть микросервис интеграции версии 1.15.0+ (пока SNAPSHOT) .. note:: Доступ к Zookeeper необходим для работы :ref:`событий`. Для работы команд достаточно доступа к RabbitMQ. Настройка ---------- Схема подключения двух инстансов Citeck: .. image:: _static/2_instances/2_instances_1.png :width: 600 :align: center Подключение пунктирными линиями настраиваются в central-config для микросервиса интеграции (в дальнейшем подключение к Zookeeper будет в connections под ключом **zookeeper**): .. code-block:: yaml # Пример конфигурации для 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** - массив из подключений к различным контурам Citeck (поддерживается неограниченное кол-во подключений). - **extecos[].this-app-name** - имя текущего контура Citeck. Будет использоваться при отправке команд и событий из внешней системы в текущую - **extecos[].connections** - раздел с подключениями к RabbitMQ и Zookeeper - **extecos[].commands** - настройка команд; - **extecos[].commands.apps** - локальные приложения, которым будет возможность отправлять команды из внешней системы. **Регистрация исполнителя команд** Инжектим **CommandsService** и вызываем: .. code-block:: java commandsService.addExecutor(new SomeExecutor()); Пример исполнителя команд: .. code-block:: java public class SomeExecutor implements CommandExecutor { // 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 позаботится о приобразовании данных). Отправка команды во внешнюю систему (из Citeck 1 (second-ecos) в Citeck 0 (main-ecos)) ----------------------------------------------------------------------------------------- **Из java кода** Инжектим **CommandsService** и вызываем отправку команды: .. code-block:: java commandsService.executeSync(builder -> { // вместо executeSync можно вызвать просто execute, чтобы не дожидаться ответа. builder.setTargetApp("main-ecos/alfresco"); // целевое приложение. Является значением this-app-name из конфигурации целевого контура Citeck + "/" + индентификатор целевого приложения 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 (Citeck 1 - second-ecos) в alfresco (Citeck 0 - main-ecos), то ход команды будет следующим: .. image:: _static/2_instances/2_instances_2.png :width: 600 :align: center Тестирование отправки команд ----------------------------- Отправка команд в удаленный Citeck и локальный отличается только аргументом в **setTargetApp**. Т.о. отлаживать механизм можно без учета нескольких инстансов Citeck. Отправка команды в локальный RabbitMQ через Java тест (можно размещать в **ecos-integrations**): .. code-block:: java 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; } } Локальное тестирование отправки команд на удаленный инстанс (имеет смысл после отладки через обычную отправку команд): 1. Добавляем настройку удаленного контура Citeck как описано в разделе **Настройка**. В качестве целевого RabbitMQ выбираем localhost. Т.о. можно локально тестировать работу с удаленными инстансами подняв только один инстанс RabbitMQ. Конфликтов при этом не возникнет. 2. Немного меняем аргумент в методе **setTargetApp** при отправке команды в тесте: .. code-block:: java ... здесь все аналогично предыдущему блоку кода, который описывает класс CommandsTest ... System.out.println(commandsService.executeSync(builder -> { builder.setTargetApp("main-ecos/alfresco"); // единственное отличие при отправке команд - добавляется идентификатор контура Citeck со слэшем builder.setType("some-command-type"); builder.setBody(new SomeBody()); builder.setTtl(Duration.of(1, ChronoUnit.MINUTES)); return Unit.INSTANCE; })); ... здесь все аналогично предыдущему блоку кода, который описывает класс CommandsTest ... При желании можно подключиться и к реальному удаленному Citeck, но для этого должен быть доступ к RabbitMQ извне. При этом достаточно будет исправить параметры в **RabbitMqConnProps** Отправка файлов в командах и событиях -------------------------------------- Для отправки файлов в командах и событиях следует использовать поля с типом byte[] (сообщения сжимаются перед отправкой. Т.е. доп. оптимизация не нужна). Для удобной работы с файлами есть утилитные классы **EcosMemFile EcosMemDir** и **ru.citeck.ecos.commons.utils.ZipUtils**, который может легко упаковывать много файлов в один поток байт и обратно. Пример: .. code-block:: java 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; });