Техническое задание

Бэкенд сервиса поиска пропавших людей

Цель

Разработать учебный прототип (MVP) системы поиска пропавших людей, включающей управление волонтёрами, учёт инцидентов, трекинг координат и уведомления.

Проект должен включать в себя:

  • версионированый REST AРІ для взаимодействия с микросервисами
  • асинхронное взаимодействие через Kafka
  • реализацию паттерна transactional outbox (либо с помощью Kafka Connect и Debezium Postges Connector, либо с помощью механизма планировщиков Spring Boot)
  • аутентификацию и авторизацию через Кеycloak
  • мониторинг, логирование и наблюдаемость с помощью механизмов Grafana Loki (аггрегация логов), Grafana Tempo (распределенная трассировка), Prometheus (сбор метрик) и Grafana (визуализация данных)
  • развертывание микросервисов либо в Kubernetes, либо в Docker с помощью Docker Compose
  • развертывание инфраструктурных компонентов либо в Kubernetes, либо в Docker с помощью Docker Compose
  • реализацию паттернов Retry, Rate Limit, Circuit Breaker
  • реализацию единой точки входа в приложение с помощью Spring Cloud Gateway Server

1. Аутентификация и авторизация

В качестве Identity Provider должен использоваться Кeycloak.
Система должна обеспечивать доступ к эндпоинтам на основе двух ролей:
  • ROLE_VOLUNTEER - зарегистрированный волонтер
  • ROLE_ADMIN - администратор системы

Все запросы к защищенным эндпоинтам извне системы должны проходить через Gateway Server и содержать токен доступа в формате ЈWT. На основе роли из токена принимается решение о предоставлении доступа к тому или иному эндпоинту.

Предлагаемый сценарий первичной регистрации волонтера

  1. Пользователь открывает начальную страницу сайта.
  2. У пользователя нет сессионной куки, соответственно система его не узнает.
  3. Пользователю доступен UI входа или регистрации.
  4. Пользователь нажимает кнопку "Стать волонтером".
  5. Система редиректит пользователя на UI Keycloak.
  6. Пользователь регистрируется в Identity Provider, заполняя ФИО, логин, пароль, email.
  7. Identity Provider редиректит пользователя по адресу логина системы с кодом авторизации.
  8. Система меняет код авторизации на токены доступа и идентификации.
  9. Система создает сессионную куку.
  10. Пользователю доступны эндпоинты согласно роли ROLE_VOLUNTEER из Токена доступа.
  11. Пользователю показывается экран с формой заполнения контактной информации о себе:
    • 1. ФИО
    • 2. Пол
    • 3. Номер телефона
    • 4. email
    • 5. Дата рождения
    • 6. Населенный пункт проживания (название)
    • 7. Район населенного пункта проживания (необязательное поле).
  12. Пользователь заполняет форму и отправляет запрос на регистрацию.
  13. Система по куке получает токен доступа из хранилища и передает его далее по цепочке.
  14. Система проверяет наличие и валидность токена доступа в запросе к эндпоинту и соответствие роли.
  15. Система проверяет корректность переданых данных.
  16. Система сохраняет данные пользователя, при этом в качестве поля user_id используется sub (идентификатор, назначенный пользователю в Identity Provider) из токена доступа.
  17. Система возвращает суррогатный, сгенерированный системой идентификатор записи в БД для зарегистрированного волонтера и ФИО волонтера.
В данном задании вам требуется реализовать шаги с 14 по 17. То есть запросы к эндпоинтам, определенным в Gateway Server должны сопровождаться токенами доступа, а сам Gateway Server должен выступать в качестве сервера ресурсов (resource server) в терминологии OAuth 2.0, валидировать токен и проверять роли, определенные в токене на соответствие разрешениям, прописанным в цепочке безопасности. В настройках Identity Provider должна быть предусмотрена возможность получения токена доступа по логину и паролю в целях организации автоматического тестирования системы.

Сценарий повторного входа по куке (после того, как пользователь ввел все регистрационные данные)

  1. Пользователь открывает начальную страницу сайта.
  2. У пользователя есть сессионная кука.
  3. Система по куке получает токен доступа и передает его далее по цепочке.
  4. Система проверяет наличие и валидность токена доступа в запросе к эндпоинту и соответствие роли.
  5. Система распознает пользователя как зарегистрированного в системе волонтера (по sub из токена доступа).
  6. Система возвращает суррогатный, сгенерированный системой идентификатор записи в БД для зарегистрированного волонтера и ФИО волонтера.
  7. Система показывает пользователю UI волонтера.

Сценарий повторного входа по куке (пользователь зарегистрировался в Keycloak, но не ввел данные регистрации в системе)

  1. Пользователь открывает начальную страницу сайта.
  2. У пользователя есть сессионная кука.
  3. Система по куке получает токен доступа и передает его далее по цепочке.
  4. Система проверяет наличие и валидность токена доступа в запросе к эндпоинту и соответствие роли.
  5. Система НЕ распознает пользователя как зарегистрированного в системе волонтера (по sub из токена доступа не найден зарегистрированный пользователь).
  6. Система возвращает ответ о том, что пользователь не найден.
  7. Система показывает пользователю UI регистрации.

Сценарий повторного входа без куки (после того, как пользователь ввел все регистрационные данные)

  1. Пользователь открывает начальную страницу сайта.
  2. У пользователя нет сессионной куки.
  3. Пользователю доступен UI входа и регистрации.
  4. Пользователь нажимает кнопку "Войти"
  5. Пользователь вводит логин и пароль в UI Keycloak.
  6. Происходит обмен кода авторизации на токены доступа и идентификации, создание сессионной куки и сохранение токенов в хранилище.
  7. Система по куке получает токен доступа и передает его далее по цепочке.
  8. Система проверяет наличие и валидность токена доступа в запросе к эндпоинту и соответствие роли.
  9. Система распознает пользователя (по sub из токена доступа).
  10. Система возвращает суррогатный, сгенерированный системой идентификатор записи в БД для зарегистрированного волонтера и ФИО волонтера.
  11. Пользователю доступен UI волонтера.

Сценарий повторного входа без куки (пользователь зарегистрировался в Keycloak, но не ввел данные регистрации в системе)

  1. Пользователь открывает начальную страницу сайта.
  2. У пользователя нет сессионной куки.
  3. Пользователю доступен UI входа и регистрации.
  4. Пользователь нажимает кнопку "Войти"
  5. Пользователь вводит логин и пароль в UI Keycloak.
  6. Происходит обмен кода авторизации на токены доступа и идентификации, создание сессионной куки и сохранение токенов в хранилище.
  7. Система по куке получает токен доступа и передает его далее по цепочке.
  8. Система проверяет наличие и валидность токена доступа в запросе к эндпоинту и соответствие роли.
  9. Система НЕ распознает пользователя (по sub из токена доступа).
  10. Система возвращает ответ о том, что пользователь не найден.
  11. Пользователю доступен UI регистрации.

Аутентификация администраторов аналогична аутентификации волонтеров, за исключением того, что регистрация в Identity Provider осуществляется вручную. В данном задании реализовывать функционал управления администраторами системы не требуется.

2. Управление волонтерами в системе поиска (Volunteer Service)

  1. Волонтер должен иметь возможность зарегистрироваться в системе:
    1. POST /api/v1/volunteer/register/me
    2. В теле запроса передается:
      1. ФИО
      2. Пол
      3. Номер телефона
      4. email
      5. Дата рождения
      6. Населенный пункт проживания (название)
      7. Район населенного пункта проживания (необязательное поле).
  2. Волонтер должен иметь возможность посмотреть данные о своей регистрации
    1. GET /api/v1/volunteer/me
  3. Волонтер должен иметь возможность удалить свою учетную запись из системы поиска пропавших людей
    1. DELETE /api/v1/volunteer/me
  4. Волонтер должен иметь возможность обновить данные о себе в системе поиска пропавших людей:
    1. PATCH /api/v1/volunteer/me
    2. Населенный пункт
    3. Район населенного пункта
    4. Фамилия
    5. email
    6. контактный номер телефона
  5. Администраторы системы должны иметь возможность получить список зарегистрированных волонтеров, используя следующие фильтры:
    1. POST /api/v1/admin/volunteer/list
    2. Населенный пункт
    3. Район населенного пункта
    4. Статус волонтера (свободен, на задании).
  6. Администраторы системы должны иметь возможность посмотреть данные любого волонтера
    1. GET /api/v1/admin/volunteer/{id}
  7. Система должна предоставлять возможность получить контактные данные волонтеров по списку идентификаторов волонтеров.
    1. POST /internal/api/v1/volunteer/list
    2. В теле запроса передается список идентификторов волонтеров
    3. Эндпоинт не выставлен наружу через Gateway Server, доступен только внутри системы.
  8. Волонтер должен иметь возможность подтвердить свое участие в инциденте:
    1. POST /api/v1/volunteer/me/incident/act
    2. В теле запросе передается идентификатор инцидента и действие (отказ / подтверждение).
    3. Поведение системы при получении подтверждения / отказа:
      1. Подтверждение:
        1. Волонтер может принимать участие только в одном инциденте. Остальные запросы отклоняются со статусом 409.
        2. Если проверка на шаге 1 успешна, отправить сообщение в топик kafka volunteer_incident_assign_event_v1
          1. идентификатор инцидента incident_id
          2. идентификатор волонтера volunteer_id (берется из БД по sub из токена доступа)
          3. статус ACCEPT
      2. Отказ:
        1. Если пришел статус REJECT и волонтер уже работает над этим инцидентом, то он прекращает работу над инцидентом.
        2. В любом случае необходимо отправить сообщение в топик kafka volunteer_incident_assign_event_v1
          1. идентификатор инцидента incident_id
          2. идентификатор волонтера volunteer_id
          3. статус REJECT
Схема сообщения Volunteer IncidentAssignEventV1
{
  "type": "record",
  "name": "VolunteerIncidentAssignEventV1",
  "namespace": "ru.cloudjava.rescue.volunteer.avro",
  "doc": "Решение волонтёра об участии в инциденте",
  "fields": [
    { "name": "event_id", "type": { "type": "string", "logicalType": "uuid" } },
    { "name": "occurred_at", "type": { "type": "long", "logicalType": "timestamp-millis" } },
    { "name": "producer", "type": "string" },
    { "name": "incident_id", "type": { "type": "string", "logicalType": "uuid" } },
    { "name": "volunteer_id", "type": { "type": "string", "logicalType": "uuid" } },
    { "name": "status", "type": { "type": "enum", "name": "AssignDecision", "symbols": ["ACCEPT", "REJECT"] } }
  ]
}
Сервис Volunteer Service не должен работать с токеном JWT, так как он валидируется на стороне Gateway Server. Вместо токена Gateway Server должен передавать необходимую информацию в заголовке X-USER-ID, подставляя в качестве значения sub из токена доступа.

Примерная модель данных:

Volunteer:
  1. id UUID primary key
  2. user_id text not null unique (sub из access_token, сгенерированный Кеycloak)
  3. first_name text not null
  4. last_name text not null
  5. middle_name text
  6. status text not null (FREE, ASSIGNED_TASK) default FREE
  7. create_date timestamptz not null default now
  8. update_date timestamptz
  9. location_id references location(id)
  10. current_incident_id UUID
Location:
  1. id UUID primary key
  2. name text not null unique
  3. parent_loc_id UUID references location(id)
  4. create_date timestamptz not null default now
  5. location_kind text not null default 'PARENT' ('CHILD')
  6. update_date timestamptz
Contactinfo:
  1. id UUID primary key
  2. contact text not null unique
  3. contact_type text not null (enum PHONE, EMAIL)
  4. create_date timestamptz not null default now
  5. update_date timestamptz
  6. volunteer_id UUID references volunteer (id)

3. Управление местонахождением волонтеров (Tracking Service)

При реализации сервиса требуется использовать расширение PostGis для хранения геолокаций и поиска по ним.
  1. Система должна принимать, хранить и обновлять данные о местонахождении волонтера по его идентификатору. Эндпоинт доступен для ROLE_VOLUNTEER.
    1. POST /api/v1/tracking
    2. В запросе передается идентификатор волонтера, текущие координаты lat широта, lon долгота
    3. Поведение системы:
      1. Сохранение или обновление записи в БД
      2. Отправка сообщения в топик Кафка volunteer_location_change_event_v1. (Ученикам предлагается подумать над различными настройками Kafka Producer и вариантами отправки сообщения: синхронный, асинхронный, с помощью паттерна Transactional Outbox, взвесить все "за" и "против" и выбрать один из вариантов, обосновав свое решение в JavaDoc)
  2. Система должна выдавать список идентификаторов волонтеров и их координат в пределах указанных координат. Эндпоинт доступен для роли ROLE_ADMIN.
    1. POST /api/v1/tracking/rectangle
    2. lat_1, lon_1, lat_2, lon_2, представляющих собой координаты левой верхней и правой нижней границ прямоугольника
    3. Если количество координат в прямоугольнике превышает заданный в конфигурации параметр ( 600 ), то тогда возвращается ответ в виде кластеров координат, с количеством координат в одном кластере. Количество кластеров также ограничено конфигурационным параметром сервера ( 800 ). Для реализации функционала рекомендуется использовать функции Postgis: ST_Transform, ST_ClusterDBSCAN, ST_Centroid, ST_Collect, ST_Y, ST_X, ST_Intersects
    4. Если координаты волонтера не обновлялись в течение конфигурируемого времени (10 дней), то они не попадают в список.
    5. При расчете кластеров все расстояния должны быть в метрах. Необходимо рассчитать параметр eps функции ST_ClusterDBSCAN в зависимости от размера прямоугольника, в котором запрашиваются координаты волонтеров. Чем больше прямоугольник, тем больше параметр eps. Также требуется учесть, что в некоторых регионах может быть совсем мало волонтеров, поэтому параметр minpoints данной функции должен быть равен единице, то есть один волонтер может составить кластер, если поблизости от него нет других волонтеров в пределах eps метров.
    6. Для более точного понимания того, что требуется реализовать, рекомендуется внимательно изучить тесты tracking-service.
  3. Система должна предоставлять текущие координаты волонтера по его идентификатору. Эндпоинт доступен для роли ROLE_VOLUNTEER и ROLE_ADMIN.
    1. GET /api/v1/tracking/volunteer/{id}
  4. Система должна предоставлять возможность получить список ближайших к указанной координате ( lat, lon ) N волонтеров и их координат. Эндпоинт доступен для роли ROLE_ADMIN, если запрос приходит извне приложения и доступен для всех сервисов самого приложения.
    1. POST /api/v1/tracking/nearest
    2. В запросе передается:
      1. количество записей, которые необходимо найти ( number )
      2. координаты геолокации lat, lon
    3. В ответе список идентификаторов волонтеров и их координат
  5. Система должна предоставлять список координат волонтеров по списку идентификаторов волонтеров, если для идентификатора волонтера из списка не найдены координаты, то возвращаются пустые значения. Эндпоинт доступен для роли ROLE_ADMIN.
    1. POST /api/v1/tracking/volunteer/list
    2. В запросе передается список идентификаторов волонтеров, координаты которых необходимо найти
    3. В ответе передается список идентификаторов волонтеров и их координат, если координаты не найдены, то возвращается только идентификатор волонтера

Примерная модель данных

Volunteer Track:
  1. id UUID primary key
  2. lat double precision not null
  3. lon double precision not null
  4. coordinates geography (Point, 4326) not null (PostGis)
  5. create_date timestamptz not null default now
  6. update_date timestamptz
  7. volunteer_id UUID not null unique
* В качестве самостоятельного развития можете реализовать механизм поиска пути волонтера за последние N часов, дней. В задании это проверяться не будет.

Модель сообщения VolunteerLocationChangeEventV1:

{
  "type": "record",
  "name": "VolunteerLocationChangeEventV1",
  "namespace": "ru.cloudjava.rescue.location.avro",
  "doc": "Событие изменения геопозиции волонтера",
  "fields": [
    { "name": "event_id", "type": { "type": "string", "logicalType": "uuid" } },
    { "name": "occurred_at", "type": { "type": "long", "logicalType": "timestamp-millis" } },
    { "name": "producer", "type": "string" },
    { "name": "volunteer_id", "type": { "type": "string", "logicalType": "uuid" } },
    { "name": "lat", "type": "double" },
    { "name": "lon", "type": "double" }
  ]
}

4. Система оповещения волонтеров и администраторов (Notification Service)

Система должна вычитывать сообщения из топика Kafka volunteer_notification_event_v1 и отправлять уведомления, на полученный в сообщении контактный телефон или email.

Модель сообщения NotificationEventV1:
{
  "type": "record",
  "name": "NotificationEventV1",
  "namespace": "ru.cloudjava.rescue.notification.avro",
  "doc": "Событие для отправки уведомлений волонтёрам/администраторам",
  "fields": [
    { "name": "event_id",    "type": { "type": "string", "logicalType": "uuid" } },
    { "name": "occurred_at", "type": { "type": "long", "logicalType": "timestamp-millis" } },
    { "name": "producer",    "type": "string" },
    { "name": "incident_id", "type": { "type": "string", "logicalType": "uuid" } },
    { "name": "incident_status",
      "type": { "type": "enum", "name": "IncidentStatus", "symbols": ["IN_PROGRESS","SUCCESS","FAIL"] }
    },
    {"name":  "params",
    "type":  {
      "type": "array",
        "items": {
          "type": "record",
          "name": "NotificationEventParamV1",
          "fields": [
            {
              "name": "param_type",
              "type": {
                "type": "enum",
                "name": "ParamType",
                "symbols": [
                  "DESCRIPTION",
                  "LAT",
                  "LON",
                  "LOCATION",
                  "PERSON_NAME",
                  "PHOTO_URL",
                  "PERSON_AGE",
                  "IDENTIFYING_MARKS",
                  "LAST_SEEN_CLOTHES",
                  "LAST_SEEN_DATE"
                ]
              }
            },
            { "name": "param_value", "type": "string" }
          ]
        }
      }
    },
    { "name": "contacts",
      "type": {
        "type": "array",
        "items": {
          "type": "record",
          "name": "Contact",
          "fields": [
            { "name": "contact_type",
              "type": { "type": "enum", "name": "ContactType", "symbols": ["EMAIL","PHONE"] }
            },
            { "name": "contact", "type": "string" },
            { "name": "volunteer_id",    "type": { "type": "string", "logicalType": "uuid" } }
          ]
        }
      }
    }
  ]
}
В текущей реализации не требуется отправлять реальные сообщения. Достаточно логировать отправку. Однако, важно учесть, что Кафка предоставляет гарантию at_least_once, то есть события могут вычитываться повторно. Требуется реализовать функционал таким образом, чтобы минимизировать или вообще свести к нулю повторные отправки. Добиться этого можно разными путями: как настройкой гарантии exactly_once в Кафка (сложный и далеко не всегда нужный путь), так и в коде приложения, вычитывающего сообщения (гораздо более легкий и зачастую более производительный путь).

Если у волонтера есть и email и телефон, то уведомление отправляется на оба контакта.

Параметры сообщения для инцидента в статусе IN_PROGRESS:
  1. Заголовок: "Уважаемый волонтер. Просьба принять участие в поиске пропавшего человека PERSON_NAME в городе LOCATION"
  2. Тело сообщения (описан состав параметров тела сообщения. Сами формулировки могут быть в произвольной форме):
    1. Если контакт EMAIL, передается вся информация об инциденте:
      Описание происшествия: DESCRIPTION.
      Координаты происшествия. Широта: LAT. Долгота: LON.
      Место происшествия: LOCATION.
      ФИО пропавшего: PERSON_NAME.
      Ссылка на фото пропавшего: PHOTO_URL.
      Возраст пропавшего: PERSON_AGE.
      Особые приметы пропавшего: IDENTIFYING_MARKS.
      Одежда, в которой последний раз видели пропавшего: LAST_SEEN_CLOTHES.
      Дата, когда последний раз видели пропавшего: LAST_SEEN_DATE.
    2. Если контакт PHONE и отсутствует EMAIL, то передается полная информация, иначе:
      "Подробная информация о пропавшем человеке отправлена вам на почту."
Параметры сообщения для инцидента в статусе SUCCESS:
  1. Заголовок: "Уважаемый волонтер. Поиск пропавшего человека PERSON_NAME прекращен."
  2. Тело сообщения:
    "Ура! Мы смогли найти PERSON_NAME живым и здоровым! Спасибо за участие в поиске!"
Параметры сообщения для инцидента в статусе FAIL:
  1. Заголовок: "Уважаемый волонтер. Поиск пропавшего человека PERSON_NAME прекращен."
  2. Тело сообщения:
    "К сожалению, нам не удалось найти PERSON_NAME живым и здоровым. Дальнейший поиск не имеет смысла. Спасибо за участие в поиске!"
Задание со звездочкой: ученикам предлагается подумать над распараллеливанием задач по отправке уведомлений. Возможны несколько вариантов распараллеливания, предлагается взвесить все за и против и выбрать наиболее оптимальный с точки зрения сложности реализации и поддержки и обосновать свое решение в виде JavaDoc. В любом случае, распараллеливание требуется вынести под Feature Toggle, чтобы можно было включить/отключить лишь сменой конфигурационного параметра.

5. Система управления инцидентами (Incident Service)

  1. Администраторы должны иметь возможность зарегистрировать инцидент:
    1. POST/api/v1/incident/create
    2. С помощью ключа идемпотентности, передаваемого в заголовке X-Idempotency-Key, необходимо обеспечить, чтобы при многочисленных запросах на создание одного и того же инцидента, в БД сохранялась только одна запись, а остальные запросы отклонялись с 409 ошибкой.
    3. В запросе передается информация о пропавшем человеке:
      1. ФИО
      2. Пол
      3. Возраст
      4. Особые приметы
      5. В чем был одет
      6. Фотография (ссылка на фото)
      7. Дата, когда человека видели в последний раз
      8. Координаты, где человека видели в последний раз (передается точка в виде lat, lon)
      9. Название населенного пункта, где человека видели в последний раз
      10. Название района населенного пункта, где человека видели в последний раз.
    4. Поведение системы при регистрации инцидента:
      1. Система должна сохранить данные об инциденте, статус инцидента IN_PROGRESS. Если в БД системы нет населенного пункта или района населенного пункта, то система добавляет соответствующие записи в БД.
      2. Система должна определить идентификаторы и координаты ближайших к инциденту волонтеров (количество волонтеров определяется в настройке, поиск координат происходит по запросу в Tracking Service).
      3. Система должна запросить контактную информацию о найденных волонтерах в Volunteer Service
      4. Система должна отправить в топик volunteer_notification_event_v1 событие с необходимой для отправки уведомления информацией о волонтерах. Параметры сообщения:
        "DESCRIPTION",
        "LAT",
        "LON",
        "DATE",
        "LOCATION",
        "PERSON_NAME",
        "PHOTO_URL",
        "PERSON_AGE",
        "IDENTIFYING_MARKS",
        "LAST_SEEN_CLOTHES",
        "LAST_SEEN_DATE"
      5. Система должна сохранить знание о том, каким волонтерам предложено участвовать в поиске пропавшего человека.
      6. Система должна вернуть список идентификаторов и координат выбранных волонтеров в ответ на запрос о регистрации задачи, а также назначенный инциденту идентификатор.
      7. Требуется обеспечить, чтобы в границы транзакции в БД не входили сетевые запросы получения данных в смежных микросервисах и отправка событий в Кафку.
      8. Отправка сообщения в Кафку должна быть реализована с помощью паттерна Transactional_Outbox.
  2. Система должна вычитывать сообщения из топика Kafka volunteer_incident_assign_event_v1 и сохранять информацию о волонтерах, которые приняли заявку на участие в поиске пропавшего человека. Требуется учесть, что сообщения могут приходить повторно (гарантия at least once) а также в редких случаях возможно переупорядочивание сообщений.
  3. Администраторы должны иметь возможность обновить статус инцидента (статусная модель: IN_PROGRESS, SUCCESS, FAIL), обновить можно статус на SUCCESS, FAIL.
    1. PATCH/api/v1/incident/status
    2. В теле запроса передается новый статус и идентификатор инцидента
    3. Поведение системы при получении статусов SUCCESS, FAIL
      1. Система обновляет статус инцидента в БД.
      2. Система получает контактную информацию волонтеров, учавствующих в поиске (запрос в Volunteer Service)
      3. Система отправляет в топик volunteer_notification_event_v1 событие с соответствующей информацией по инциденту. Параметры сообщения: PERSON_NAME
      4. Отправка сообщения в Кафку должна быть реализована с помощью паттерна Transactional Outbox.
      5. Требуется обеспечить, чтобы в границы транзакции в БД не входили сетевые запросы получения данных в смежных микросервисах и отправка событий в Кафку.
  4. Администраторы должны иметь возможность получить всю информацию об инциденте и о принимающих участие в поиске волонтерах
    1. GET/api/v1/incident/{id}
    2. В ответе должна быть следующая информация:
      1. Идентификатор инцидента
      2. Информация о пропавшем человеке
      3. Дата, когда человека видели в последний раз
      4. Координаты, где человека видели в последний раз (передается точка в виде lat, lon)
      5. Название района населенного пункта, где человека видели в последний раз
      6. Статус инцидента
      7. Список идентификаторов волонтеров, участвующих в поиске, и их текущих координат (при наличии)
  5. Администраторы должны иметь возможность получить информацию об инцидентах:
    1. POST/api/v1/incident/list
    2. В теле запроса передаются фильтры, по которым необходимо отфильтровать список инцидентов, а также информация о пагинации списка инцидентов (страница, количество записей на странице):
      1. Населенный пункт
      2. Район населенного пункта
      3. Год инцидента
      4. Месяц инцидента
      5. Статус инцидента
      6. pageFrom
      7. count
  6. Система должна вычитывать сообщения из топика Кафка volunteer_location_change_event_v1 и обновлять координаты волонтера в своей БД, если его идентификатор есть в БД.
  7. Волонтер должен иметь возможность получить постраничный список инцидентов, в которых он принимал участие:
    1. POST/api/v1/incident/volunteer/list
    2. В теле запроса передается:
      1. идентификатор волонтера
      2. pageFrom
      3. count
  8. Волонтер должен иметь возможность получить постраничный список инцидентов, в которых ему предлагается принять участие, но он еще не ответил согласием или отказом. После того, как волонтер отказывается или соглашается принять участие в инциденте, данный инцидент исключается из этого списка:
    1. POST/api/v1/incident/volunteer/pending/list
    2. В теле запроса передается:
      1. идентификатор волонтера
      2. pageFrom
      3. count

Примерная модель данных

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

Incident
  1. id UUID primary key
  2. description text not null
  3. closest_lat double precision not null
  4. closest_lon double precision not null
  5. create_date timestamptz
  6. update_date timestamptz
  7. status text not null default IN_PROGRESS (IN_PROGRESS, SUCCESS, FAIL)
  8. location_id references location(id)
LostPerson
  1. id UUID primary key
  2. first_name text not null
  3. last_name text not null
  4. middle_name text
  5. photo_url text
  6. age integer
  7. identifying_marks text
  8. last_seen_clothes text
  9. last_seen_date timestamptz not null
  10. incident_id UUID not null references incident(id)
Location
  1. id UUID primary key
  2. name text not null unique
  3. parent_loc_id UUID references location(id)
  4. create_date timestamptz not null default now
  5. location_kind text not null default 'PARENT' ('CHILD')
  6. update_date timestamptz
Assigned_Volunteer
  1. id UUID primary key not null
  2. volunteer_id UUID not null
  3. assign_date timestamptz not null
  4. last_known_lat double precision
  5. last_known_lon double precision
  6. incident_id UUID not null references incident(id)

6. Нефункциональные требования

  1. Поддержка rate_limiting на уровне Gateway Server
  2. В логах номера телефонов и email должны маскироваться следующим образом:
    1. email: someemail@service.com --> s***l@service.com
    2. телефон: 79999999999 --> 7999***9999
  3. В Prometheus должны быть следующие метрики помимо стандартных:
    1. incident_count_total (количество инцидентов в регионе): метка region, значение — название населенного пункта (или района населенного пункта)
    2. incident_result_total: метка status, значение — success или fail в зависимости от достигнутого результата поиска.
  4. При организации межсервисного взаимодействия если в результате идемпотентного запроса получена сетевая ошибка или 500 ответ от сервиса, то запрос должен быть повторен указаное в конфигурации сервиса количество раз с экспоненциальной или фиксированной задержкой в зависимости от сценария взаимодействия.
  5. При организации межсервисного взаимодействия необходимо реализовать паттерн Circuit Breaker
  6. Зависимые от окружения (dev, qa, prod) конфигурации должны попадать в сервисы с помощью переменных окружения
  7. Необходимо использовать Spring Boot версии не ниже 3.5.3
  8. Сервисы должны быть покрыты Unit и интеграционными тестами.
  9. Для организации хранения данных в БД необходимо использовать механизм миграций Flyway или Liquibase.
  10. Все даты должны храниться в БД в зоне UTC.
  11. При работе с БД необходимо использовать время сервера приложения, а не время сервера БД.
  12. При развертывании в Kubernetes передача логинов и паролей от БД и других систем должна осуществляться с помощью абстракции Secret. Задание со звездочкой (необязательно к реализации) — настроить систему хранения секретов Vault и передавать секреты в приложение с помощью SideCar контейнера vault-agent, описывая секреты в аннотациях Deployment приложения.
  13. При реализации слоя данных разрешено пользоваться технологиями Spring Data JPA (Hibernate), Spring Data JDBC, Spring Data R2DBC.
  14. При организации асинхронного взаимодействия через брокер сообщений Apache Kafka сообщения должны быть в формате Avro.
  15. Устаревшие и более ненужные в БД данные должны очищаться автоматически самим приложением.

7. Запуск автотестов

Для тестирования системы подготовлен набор автотестов. Рекомендуется внимательно изучить тесты каждого микросервиса, чтобы понять ожидаемое поведение в различных сценариях как успешных, так и неуспешных. Разобравшись в том, что ожидается в тесте, вы ответите на большинство вопросов, которые у вас могут возникнуть по ходу реализации.

Если ваш проект разворачивается в кластере Kubernetes на локальном хосте с помощью Minikube, необходимо выполнить следующие шаги, чтобы запустить автотесты:

  1. Добавить в конфигурацию Kafka advertised listener на localhost:9093. Для этого требуется:
    1. Добавить протокол безопасности для него в переменную окружения KAFKA_LISTENER_SECURITY_PROTOCOL_MAP
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,PLAINTEXT_TEST:PLAINTEXT
      Тут PLAINTEXT_TEST:PLAINTEXT используется для тестов
    2. Добавить тестовый листенер в переменную окружения KAFKA_LISTENERS
      KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:29093,PLAINTEXT_TEST://:9093
    3. Добавить тестовый листенер в переменную окружения KAFKA_ADVERTISED_LISTENERS
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka-0.kafka.default.svc.cluster.local:9092,PLAINTEXT_TEST://localhost:9093
  2. Запустить все приложения и инфраструктурные компоненты
  3. Прокинуть порты на localhost для Kafka, Postgres и Schema Registry:
    1. kubectl port-forward kafka-0 9093
    2. kubectl port-forward postgres-0 15432
    3. kubectl port-forward SCHEMA_REGISTRY_POD_NAME 8081 — требуется заменить SCHEMA_REGISTRY_POD_NAME на название поды с Schema Registry
  4. Запустить туннель для вашего кластера minikube tunnel -p YOUR_CLUSTER_NAME
  5. Не забудьте добавить в /etc/hosts маппинги (сами хосты можете выбрать другие, важно, чтобы все было настроено консистентно) по аналогии с тем как это было сделано в курсе по Cloud Java K8S.
    127.0.0.1 keycloak.rescue-service.ru
    127.0.0.1 grafana.rescue-service.ru
    127.0.0.1 prometheus.rescue-service.ru
    127.0.0.1 rescue-service.ru
  6. Не забудьте поправить coreDns, чтобы запросы к Keycloak были корректны как извне кластера, так и изнутри.
  7. Обязательно прочитайте Readme.md в проекте с автотестами и обновите конфигурацию в application.yml согласно рекомендациям, если это требуется.
Основное требование для запуска автотестов: обновить конфигурацию проекта с тестами таким образом, чтобы было доступно подключение к Gateway Service, Kafka, Postgres, Schema Registry, Keycloak.

8. Схемы OpenApi

9. Предлагаемая архитектура проекта:


10. Рекомендации по реализации

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

  1. Определитесь со стеком технологий для конкретного микросервиса: нужен ли в нем Spring Data JPA или возможно легче будет работать с Spring Data JDBC, требуется ли подключение к Kafka, какие зависимости нужны для организации Monitoring, Observability, Tracing и Logging и т.п.
  2. Декомпозируйте задачу на подзадачи. В рамках ТЗ уже определена декомпозиция по микросервисам и эндпоинтам. Вам требуется декомпозировать реализацию каждого отдельного эндпоинта. Можно пойти различными путями, например:
    1. API -> сервисный слой -> слой данных
    2. Слой данных -> сервисный слой -> API
  3. При реализации старайтесь распределять задачи таким образом, чтобы планировать их реализацию в рамках двухнедельных спринтов, как это зачастую бывает в крупных компаниях.
  4. Каждую задачу выполняйте в отдельной feature ветке, после выполнения задачи готовьте Pull Request в ветку develop и проводите ревью своего решения. Старайтесь критично подходить к ревью — обращайте внимание на соответствие решения техническому заданию, на наличие возможных ошибок (особенно часто упускают из вида NPE), стиль кода (рекомендуется придерживаться одного стиля в рамках всего проекта), распределение классов по пакетам, следование принципам SOLID, покрытие тестами и т.п. Pull Request не стоит мержить, если в нем отсутствуют тесты нового функционала. После мержа Pull Request-а, удаляйте более ненужную feature-ветку, чтобы не засорять проект.
  5. В конце каждого двухнедельного спринта подводите итог того, что уже сделано и планируйте задачи на следующий спринт.
  6. При реализации конкретного эндпоинта внимательно изучите тесты на него из проекта с автотестами. Вероятнее всего вы найдете ответы на большинство вопросов.
  7. Если вы столкнулись с трудностью реализации, не опускайте руки. Сформулируйте свой вопрос — зачастую в процессе формулирования вопроса находится и ответ на него. Обязательно гуглите и старайтесь найти решение самостоятельно. Если вы задаете вопрос ИИ, учитывайте, что он очень часто дает неверные советы, поэтому обязательно читайте официальную документацию и исходный код, так вы сможете «довести до ума» предлагаемое ИИ решение. Если в результате поиска решения вы ни к чему не пришли, то задайте вопрос в чат поддержки, вам обязательно помогут. При этом четко опишите проблему и как вы пытались ее решить. Рассматривайте обращение в чат как обращение к тим-лиду, который поставил вам задачу на реализацию. Другими словами, не стоит приходить с вопросом — «Как реализовать такой-то функционал, что-то я не разобрался?»
  8. Когда вся система будет готова, обязательно прогоните автотесты. Если они проходят успешно, значит вы справились с заданием. Если же возникают ошибки, то не спешите расстраиваться — с первого раза выполнить такую сложную работу практически невозможно. Внимательно изучите ошибку, прочитайте логи каждого задействованного в процессе микросервиса, локализуйте проблему и вы найдете ее решение. Такого рода исправления стоит делать так же в отдельных bugfix ветках с Pull Request-ами и код ревью. На каждую найденную ошибку заводите свой bugfix. Не исправляйте все в одном Pull Request-е. Так будет проще отслеживать историю изменения проекта. Логику тестов можно интегрировать в код конкретного микросервиса, чтобы проверить его в изоляции. Однако в этом случае придется мокировать ответы от смежных микросервисов, для этого рекомендуется использовать WireMock.
  9. Стоит отметить, что проект достаточно сложный как с технической, так и с бизнесовой точки зрения. Вам зачастую придется искать компромиссные решения. Если вы видите несколько возможных вариантов решения той или иной задачи и хотите опробовать их все, то заносите их под feature-toggles, а по умолчанию включайте тот, который считаете наиболее подходящим.

11 Референсная реализация

В референсной реализации для вас подготовлены следующие проекты:

  • volunteer-service
  • tracking-service
  • incident-service
  • notification-service
  • volunteer-gateway-service
  • rescue-service-k8s

Также подготовлено 2 варианта деплоя микросервисов:

  1. Docker: в каждом микросервисе есть свой docker-compose.yml файл, в котором определен необходимый для запуска этого микросервиса набор компонентов. В volunteer-gateway-service определена окончательная конфигурация для деплоя в Docker.
  2. Kubernetes (отдельный проект rescue-service-k8s)

Для локального запуска с деплоем в Kubernetes вам необходимо:

  1. Опубликовать все микросервисы в своем Github-репозитории.
  2. Запустить Github Actions в вашем репозитории: сейчас настройки такие, что Github Actions запускаются при Pull Request-е в ветку develop или push в ветку k8s.
  3. В helm-чарты добавить чарт с вашим секретом для скачивания образов. Секрет надо назвать github-registry по аналогии с тем, как делали в курсе по k8s.
  4. В helm-чартах микросервисов поменять в values.yaml ссылку на образ из вашего репозитория.
  5. Прописать в /etc/hosts нужные урлы по аналогии с тем как делали в курсе по k8s:
    127.0.0.1 keycloak.rescue-service.ru
    127.0.0.1 grafana.rescue-service.ru
    127.0.0.1 prometheus.rescue-service.ru
    127.0.0.1 rescue-service.ru
  6. Настроить свой локальный кластер Minikube по аналогии с тем, как делали в курсе по k8s:
    1. выделить ресурсы
    2. добавить addon ingress
    3. пофиксить coreDns, чтобы запросы к Keycloak проходили как изнутри кластера, так и снаружи
  7. После запуска и настройки кластера, надо перейти в репозиторий rescue-service-k8s в директорию helm и выполнить команду helmfile apply (если helmfile не стоит, то установите его с официального сайта или вручную установить каждый релиз.
  8. Также необходимо запустить туннель Minikube и выполнить port-forward для Kafka, Postgres, Schema Registry.

После этого можно запускать тесты.