Нам нужны твои мозги

Хотите расти как разработчик и найти крутую работу? Не протирайте штаны — займитесь Open Source проектами. Так легче всего попасть в лучшие команды разработчиков и положить себе в резюме настоящий проект, вместо нелепых «примеров кода». Но найти подходящий проект для участия сложно. Начинаются лень и отговорки, а за ними — отсутствие профессионального роста, критики по-настоящему крутых программистов, уныние и застой.

На Cult of Martians мы собираем интересные задачи для современных веб-программистов. Можно выбрать подходящую по сложности, продолжительности и специализации. Задачи не выдуманы «из воздуха» — каждая решает насущную проблему, и решить ее можно через создание нового Open Source проекта или улучшение существующего. Решайте задачи, прокачивайтесь, присылайте решение на оценку. Лучших могут пригласить к себе на работу компании, программистам которых понравится ваше решение.

Помощь запрашивалДолганов СергейДолганов Сергей Долганов Сергей

ПомогМеркушин МихаилМеркушин Михаил Меркушин Михаил

Бэк: Гемизация «простейших контрактов» на Ruby

Для продвинутых, задача на неделю

Контракт — это набор валидаций (тестов), которые запускаются во время production сессии взаимодействия с внешней системой по API (например, CRM системой или социальными сетями).

Такие валидации решают проблему упрощения отладки и распознавания отклонений в поведении внешних зависимостей. Пример реализации — poro_contract.

Нужно создать новый gem (назовем его simple_contracts), который:

  1. внутри работает по таким же принципам, как POROContract, или просто оборачивает его в gem;
  2. является самой простой реализацией «контрактов» на Ruby в виде gem с минимумом зависимостей;
  3. обладает способностью прогонять проверку «контракта» параллельно, не влияя на основной поток исполнения (в случае использования IO в теле контракта).

Польза: получить отличный опыт с многопоточным программированием в Ruby; освоить контрактный подход к тестированию API.

Постановка задачи

Базовый контракт реализован в poro_contract. К сожалению, это не gem и там не хватает одного важного куска.

На практике мы столкнулись со следующей проблемой. Пусть в приложении реализован клиент к разным социальным сетям (Twitter, Facebook, VK) и публикация идет от имени официальной страницы или группы. Нужно обезопасить себя от ошибочных публикаций, поэтому в приложении добавлена возможность удалить пост из всех социальных сетей. И только через месяц после очередного такого удаления разработчик может узнать, что социальная сеть иногда не удаляет пост на самом деле, хотя вызов API и ответ от API был корректным.

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

# SimpleContract - should not support #match_async!
# SimpleContract[:parallel] - marks that inherited Contract, should be thread-safe and could run in parallel with #match_async!
class TwitterContract < SimpleContract[:parallel]
  # ... other rules

  def guarantee_verified_delete
    return true if @request.path !~ %r{statuses/destroy}
    return true if Twitter::REST::Client.new(@credentials).statuses(@post.tweet_id).empty?
    false
  end
end

# Use synchronously, (raises exception, "Fails Fast"™):
@post = Post.find(params[:post_id])
TwitterContract.new.match! { response = TwitterAPI.destroy(@post) }

# Use asynchronously, (does not affect TwitterAPI.destroy, but tracks any problems with TwitterContract validation):
TwitterContract.match_async! { response = TwitterAPI.destroy(@post) }

Советы по реализации

Архитектурных решений видится несколько:

  • распараллеливать всю проверку контракта;
  • распараллеливать только отдельные правила, содержащие IO.

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

На что обратить внимание:

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

Инструкции по выполнению

  1. Посмотреть реализацию и пример использования контрактов poro_contract.
  2. Завернуть решение в новый гем (например, simple_contracts) и покрыть тестами.
  3. Продумать архитектурное решение для контрактов с IO операциями.
  4. Создать pull request с обоснованием и реализацией асинхронной проверки контракта.
  5. Покрыть новую функциональность тестами.

P.S.: Есть еще gem blood_contracts — в нем параллельное выполнение работает ненадежно, поэтому ориентироваться на него не стоит. BloodContracts позиционируется как gem более сложно устроенный, a la “Rails for Contracts”. Поэтому simple_contracts должен в плане простоты быть его антиподом.