Ruby, Windows, DLL

Marat Zasorin
rnds
Published in
4 min readOct 15, 2020

Задача

Мы — компания, которая видит финтех-будущее в глобальном и унифицированом взаимодействии всех финансовых институтов и сервисов. Но пока, к сожалению, это лишь мечты. Каждый участник финансового рынка предоставляет партнерам те решения, которые считает правильными — это может быть RESTful API, локальный gateway или даже библиотека .dll, без возможности перекомпилировать ее в формат .so.

С интеграцией такой библиотеки нам и пришлось столкнуться. Библиотека дает возможность пользователю осуществлять торговые операции на бирже. Она представляет собой “черный ящик” с набором методов для инициализации, подключения, установки callback-функции, отправки команд, отключения и деинициализации. Обмен информацией между клиентом и библиотекой осуществляется xml-сообщениями.

Реализация

Выбранная реализация предполагает наличие 3 стороны взаимодействия:

  1. Веб-интерфейс;
  2. Серверное приложение для агрегирования данных;
  3. Приложение-адаптер библиотеки.

Через веб-интерфейс пользователь авторизуется, осуществляет торговые операции, а также может ознакомиться с финансовыми показателями своего профиля. Взаимодействие с серверной частью происходит по HTTP-протоколу, с программой-адаптером по WebSocket-соединению.

Серверная часть отвечает за хранение информации, полученной как от библиотеки, так и от других поставщиков, например, API брокера. Серверное приложения представляет собой точку аккумулирования всех финансовых данных о пользователе. Взаимодействие серверного приложения с адаптером осуществляется по HTTP.

Адаптер отвечает за обработку входящих от клиента запросов и отправку полученных от сервера брокера данных в серверное приложение для хранения. После успешной инициализации, подключения и установки callback-функции, библиотека готова исполнять команды. Исполнение команды происходит в два этапа:

  1. Синхронный ответ на принятую команду. Осуществляется синтаксический разбор команды и выполнение служебных задач на стороне клиента.
  2. Асинхронный ответ. Ответа сервера партнёра отправляется в callback-функцию.

Проблемы

При выбранной реализации возникает несколько проблем:

  1. Каждому пользователю требуется отдельный экземляр подключения, поскольку библиотека не рассчитана на параллельные подключения — предполагается, что библиотеку будет использовать один пользователь на одном устройстве.
  2. Асинхронный характер работы библиотеки, а также необходимость ее персистентного состояния препятствуют использованию привычной stateless-архитектуры;
  3. Согласно документации обработка ответов не должна выполнять операции, которые могут заблокировать поток исполнения на длительное время.

Решения

Параллельность

Реализация адаптера осуществляется на языке Ruby. Интерфейс для динамически загружаемых библиотек обеспечивает гем FFI.

Гем для загрузки библиотеки в адресное пространство процесса используется системный вызов dlopen. Вызов принимает имя библиотеки и возвращает указатель на ее начало. То есть при повторном вызове будет возвращен указатель на уже инициализированный инстанс библиотеки и подключение второго пользователя становится невозможным. Однако можно указать путь к копии библиотеке и таким образом обойти это ограничение — тогда будет возвращен указатель на начало этой копии.

Гем FFI использует статические методы класса для указания пути к библиотеке. Есть несколько вариантов решения этой проблемы:

  • Использовать встроенные методы Ruby (системные вызовы Dl.dlopen() );
  • Использование гема Feddle с поддержкой динамического указания пути к библиотеке;
  • Перенести статические методы гема в динамическую область.

Был использован последний способ, как самый простой и быстрый. Создается класс со статическим методом, принимающим путь к библиотеке, для создания класса инициализатора. Таким образом мы можем инициализировать библиотеку для каждого пользователя.

Для оптимизации (экономии места на жёстком диске) при инициализации используется жёсткая ссылка на библиотеку.

Персистентность

Веб-интерфейс взаимодействует с адаптером по WebSocket-соединению. В качестве сервера используется standalone cable server. Название канала и название жёсткой ссылки соответствует логину пользователя. После установления соединения выполняется создание коннектора — загружается библиотека, выполняются различные служебные команды,

После создания экземпляра загруженной библиотеки выполняется инициализация и подключение. Если подключение выполнилось успешно, экземпляр коннектора помещается в синглтон-объект, представляющий из себя key-value storage, где ключом является логин пользователя, а значением — подключенный коннектор.

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

За обработку http-запросов cable сервера отвечает puma. Если у пользователя будет открыто несколько вкладок, необходимо отправлять запросы в один и тот же инстанс сервера с синглтон-объектом, содержащим все экземпляры инициализированных библиотек. Для этого необходимо запретить пуме порождать дочерние процессы, запуская сервер командой:

puma -t 1:16

Многопоточность

В документации к библиотеке настоятельно рекомендовано избегать выполнение операций, потенциально способных заблокировать поток исполнения на продолжительное время. Для этого был реализован event-subscribe режим обработки ответов. Приём и отдача сообщений запущены в двух отдельных тредах: командном треде и треде обработки. Командный тред следит за очередью отправки команд в библиотеку, тред обработки за очередью пришедших ответов, оповещая подписанных обработчиков о новом событии.

Используя такой подход, можно гарантировать:

  • что любые действия, производимые над ответами не будут блокировать поток исполнения библиотеки;
  • что отправленные или принятые сообщения будут обработаны в том порядке, в котором были помещены в очереди и не будут потеряны.

Перспективы

Невозможность получить библиотеку в unix-совместимом формате препятствует использованию стандартных отлаженных в нашей компании CI/CD подходов, автоматического тестирования и прочих привычных жизненных циклах разработки приложения. Однако эту проблему можно решить переписав адаптер в компилируемое приложение (например Go), открываемое в unix-среде в окружении wine.

Ну пока.

--

--