суббота, 19 декабря 2020 г.

Тёмные глубины многопоточности

 Почти год назад делал тестовое задание для одной компании. Но тогда проходил собеседования сразу в несколько контор и было довольно много работы и домашних дел. Плюс, решил в своём решении показать сразу всё, на что был способен и сильно переусложнил его для тестового задания. Поэтому сделать тогда задание за два выходных дня не успел. Застрял на дедлоках и не смог быстро найти места, где они возникают.

То решение было стыдно показывать ревьюеру. Но ещё хуже было не показать ничего. Поэтому у меня весь год “чесались руки” сделать его как следует. И вот, наконец, этот час настал.
Решение получилось без изысков. Я знаю, что многое в нём можно доделать и улучшить. Но, выполняя задачу, своей цели я достиг, и решение на правильных данных работает быстро и считает, как надо.

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

Возвращаюсь к теме статьи. Если коротко, то задание звучало так: написать программу, которая будет парить данные из файла и для разных данных (разных инструментов сделок) считать разные статистики. Текст задания можно посмотреть в моём GitHub-репозитории.

Решение задачи состоит из следующих частей:
- парсер файлов. Как понятно из названия, его задача - открыть файл и распарсить его содержимое;
- модель - представляет собой распарсенные данные;
- очереди - содержат распарсенные данные для разных инструментов (число очередей равно числу инструментов, для которых необходимо посчитать статистику);
- калькуляторы статистики - рассчитывают статистику для разных инструментов сделок;
- класс, содержащий основную связующую логику приложения.

Рассмотрим их более подробно:
Processor.
Класс с основной логикой приложения. В нём мы создаём объекты классов нашего приложения, очереди для данных для калькуляторов, защёлку CountDownLatch и пул потоков, в которых параллельно будут исполняться: парсер и калькуляторы. После завершения выполнения своих задач парсером и калькуляторами, производится вывод результатов в консоль.

Parser.
Построчно считывает данные из файла и складывает их в очереди. После завершения своей работы, помещает в каждую очередь маркерную модель, которая будет сигнализировать потребителям данных, что новых даных не будет и пора завершать работу. Также  по завершении своей работы Parser открывает защёлку CountDownLatch - это сигнал для начала работы калькулятором, считающим статистику по десяти самым последним по дате данным инструмента №3.

Очереди.
Две очереди являются экземплярами LinkedBlockingQueue.
Если при вызове метода put() такой очереди она была заполнена полностью, выполнение метода блокируется в ожидании, когда в очереди появится свободное место.
Если при вызове метода take() такой очереди она была пуста, выполнение метода блокируется в ожидании, когда в очереди появится хотя бы один элемент.
Очередь BoundedPriorityBlockingQueue является кастомной реализацией блокирующей очереди с приоритетом. Её задача - хранить в себе только 10 записей. Приоритет на помещение в эту очередь имеют последние по дате данные для инструмента №3. Приоритет определяется с помощью метода  compareTo(), реализованного в модели Quotation.

Калькуляторы.
AverageValueByMonthCalculator - считает среднее значение цены за определённый месяц.
AverageValueCalculator - считает среднее значение цены за всё время.
LastTenSumCalculator - считает сумму последних десяти по дате цен.
Калькуляторы считают данные каждый из своей очереди. И прекращают работу после получения из своих очередей маркерных данных.
Калькулятор LastTenSumCalculator начинает свои расчёты после того, как Parser опустит защёлку CountDownLatch, что будет означать, что в очереди лежат последние 10 по дате данные для инструмента №3 и помещение туда новых данных не предвидится.

Благодаря такой реализации, парсинг данных и подсчёт статистики производятся параллельно и практически одновременно.
Потоковая обработка данных при помощи блокирующих очередей защищает приложение от переполнения памяти. Файл для парсинга может быть какого угодно большого размера.
Завершение парсером работы путём помещения в очередь маркерных данных, защищает приложение от live lock.


Apache Ignite cache: добавляем конфигурацию для логгера

 Итак, наш сервис стартует в окружении Apache Ignite Service Grid, загружает файл с данными и парсит данные в Кеш.
Что дальше?
Давайте, попробуем настроить вывод логов нашего сервиса в отдельный файл, чтобы они не смешивались с логами кластера Ignite и мы могли в любой момент быстро их просмотреть.
Для этого нам понадобятся две вещи:
1) Создать файл с конфигурацией для логгера.
2) Дать понять нашему сервису, что писать логи он теперь должен в новый логгер.

По первому пункту - был создан файл ignite-logback.xml
. В нём были заданы параметры для сохранения логов в обычный лог-файл, в json-файл и в консоль.
По второму пункту - добавим в метод инициализации нашего сервиса следующий код:

Здесь мы получаем контекст логгера, сбрасываем его конфигурацию, которая была подтянута в момент старта окружения Apache Ignite, и указываем конфигуратору путь к нашему файлу с конфигурации логгера - ignite-logback.xml.

Стартуем наш сервис и видим следующие логи:

Которые имеют следующее содержание:
log-файл:

Json-файл: