Данная статья является изначально была транскриптом к докладу на конференции Railsclub 2017. По итогам конференции в текст были внесены незначительные исправления.
В процессе чтения вы поймете, почему нет смысла сразу описывать все детали в мельчайших подробностях. Я не снимаю с себя ответственности в вопросе предоставления интересующей информации. Вы можете написать в комментариях, какие вопросы у вас возникли и что стоит подробнее изложить. Это послужит триггером для детальных публикаций.
Поехали.
Сегодня я хочу поговорить по поводу темы, которая может показаться окружающим старой, избитой и не интересной. Но все, на самом деле не так. Как говорится, все новое, это хорошо забытое старое, особенное если старое должно быть обновлено. Я думаю, что вы слышали о том, что все доклады делятся на 2 типа: похвастаться или исповедаться. Сегодня я буду делать второе (наверное).
В процессе подготовки к докладу, я думал как лучше начать повествование… Придумал слезливую историю про свой опыт борьбы с воздушными замками, нашел пугающие картинки… А потом зашел на википедию и… нашел ответ на вопрос, который хочу раскрыть.
Конфигурация программного обеспечения — совокупность настроек программы, задаваемая пользователем, а также процесс изменения этих настроек в соответствии с нуждами пользователя.
Проблема в том, что большинство смотрит только на первую часть этого определения, и (очень часто) не берут во внимание вторую часть. Конечно, проблема не только в этом, ключевые аспекты мы рассмотрим, но именно этот вопрос сподвиг меня пересмотреть свои взгляды на тему конфигурации проектов и выработать новый подход. И вопрос не только в том, как обновлять конфигурацию в прямом смысле этого слова. Нет, я не буду говорить о том, как нужно нажимать на клавиши, для того, чтобы породить код. Я хочу затронуть проблему внесения изменений в конфигурацию, доставку этих изменений до продакшена и работой в разных окружениях с одним и том же кодом. Вот об этом я и поговорю с вами сегодня. И да, разговор будет в контексте ruby, конечно (но можно транслировать на другие языки и технологии) :)
Начну я с небольшого лирического отступления. Вот тут многие говорят, что Ruby умер, что он не развивается, что мир тлен и надо валить. Так получилось, что за последние 3 года я увидел ряд совершенно разных проектов. Разных не только в плане предметной области, но и в плане архитектуры. Это были и монолиты, и полу-монолиты, и полу-полу-монолиты, и проекты на микросервисной архитектуре. И проекты на MRI, и на JRuby, и примеси на других языках. Зоопарк, одним словом. Так вот, в каждом из этих проектов применялись разные подходы к конфигурации этих проектов. Подходы были разные, проблемы тоже были разные. Но все эти проблемы сводились к одному и тому же слову: конфигурация. Так получилось, что при участии в одном проекте у меня просто начало, как говорится, пригорать. Компетентность и профессионализм парней в команде не вызывали никаких вопросов, но вот подходы и определенные решения… я долго не мог понять, почему принимались именно такие решения, но видел что они ведут в никуда. А потом понял. Проблема была на поверхности: К чему привыкли - то и исповедуем. Поэтому вопрос Привычки рассмотрю в первую очередь. Когда мы говорили с организаторами конференции Railsclub по поводу этого доклада, я им отправил вот это видео. С тех пор прошло достаточно количество времени, чтобы я смог подобрать политкорректные слова, систематизировать свою ненависть и спокойно все изложить.
Самое смешное в этой истории то, что экосистема rails построена на соглашениях, а именно вопрос конфигурации не подчиняется этому правилу и в этом вопросе кто во что горазд. Да, это просто такой вот парадокс. У нас будут соглашения, чтобы не было тонны конфигураций, но от конфигурации все равно не уйти.
Если кто не знает, Сonvention over Сonfiguration — это принцип построения фреймворков и библиотек, призванный сократить количество требуемой конфигурации без потери гибкости. Обычно переводится как «соглашения по конфигурации». В строгой форме этот принцип можно выразить так: аспект программной системы нуждается в конфигурации тогда и только тогда, когда этот аспект НЕ удовлетворяет некоторой спецификации. Принцип работает когда речь идет о маппинге классов на какие-либо ресурсы (таблицы базы данных, события, ресурсы файловой системы). Согласно принципу, если класс соответствует соглашению наименования, тогда он не нуждается в дополнительной конфигурации. В этом контексте название принципа можно перевести как «Соглашение НАД конфигурацией», такой перевод указывает на первостепенность соглашения, а не конфигурации.
Классический пример CoC принципа — Hibernate. В Hibernate правила объектно-реляционного меппинга можно описывать с помощью XML-файлов:
<class name="Tag" table="tag">
<property name="Name" column="name"/>
<property name="Value" column="value"/>
<property name="Value2" column="value2"/>
<property name="Value3" column="value3"/>
</class>
Как видно, здесь имеются повторения свойств класса и колонок таблицы. Если ввести соглашение о том, что по умолчанию колонки таблицы должны назаваться также как свойство, то можно опустить часть конфигурации:
<class name="Tag">
<property name="Name"/>
<property name="Value"/>
<property name="Value2"/>
<property name="Value3"/>
</class>
Однако, если какое-то свойство потребуется сохранить в колонку с отличным именем, это придется указать явно. В этом и состоит суть принципа.
Наибольшее применение принцип CoC находит в среде Ruby on Rails. Это в принципе понятно, если учесть, что RoR ориентирована на быструю разработку, а CoC позволяет свести конфигурацию к минимуму.
И так-то это правильно, но! Но это ни в коем виде не снимает ответственности с вопросов конфигурации. Не правильно считать, что все проблемы за вас уже решены другими людьми и если что-то в вашей работе выходит за пределы гайдов, то можно “немножко поговнокодить”.
Хотя, я не с того начал. Сначала стоит спросить, кто из вас работает над проектом-монолитом? Вот этим людям, скорее всего, статья в первой его части не интересной, ибо все намного проще (нет, не просто, проблемы есть), когда у вас приложение - монолит. Что мы имеем: все изменения хранятся в одном месте, их намного проще синхронизировать. Очень часто, с монолитами никаких серьезных проблем не возникает. А если и возникают какие-то конфликты в плане конфигурирования - они не причиняют много боли. Немного абстрактных костылей, немного террора в сторону DevOps и все, проблемы, в большинстве своем, решены. Но вот когда же оно начинает разделяться на маленькие сервисы (советую с этим не торопиться, если не знаете во что ввязываетесь) - тут возникает много вопросов касательно того, как поддерживать консистентность, и, в том числе, в конфигурации. Ведь все прекрасно понимают, что чем больше независимых компонентов в системе, тем сложнее их синхронизировать. А еще подключается правило “работает - не трогай”, о котором мы поговорим позже :).
Поэтому у меня сегодня несколько целей:
А для этого нужно посмотреть на ретроспективу, без этого никак. Все помнят, как эволюционировали подходы к конфигурации? Кстати, все заметили, что я не спросил еще ни у кого, что по их мнению является конфигурацией?
Самый простой, быстрый и, в некоторых случаях, незаменимый вариант. Достаточно вспомнить как описывается версия ruby gem’а.
# lib/persey/version.rb
module Persey
VERSION = "0.0.11"
end
# persey.gemspec
# ...
Gem::Specification.new do |spec|
# ...
spec.version = Persey::VERSION
# ...
end
Подход, с использованием констант для в роли инструмента для конфигурации я вижу очень часто, особенно в различных библиотеках.
# https://github.com/rails/rails/blob/master/actioncable/lib/action_cable/gem_version.rb
module ActionCable
def self.gem_version
Gem::Version.new VERSION::STRING
end
module VERSION
MAJOR = 5
MINOR = 2
TINY = 0
PRE = "alpha"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
end
Или вот:
# https://github.com/rails/rails/blob/master/actioncable/lib/action_cable.rb
module ActionCable
extend ActiveSupport::Autoload
INTERNAL = {
message_types: {
welcome: "welcome".freeze,
ping: "ping".freeze,
confirmation: "confirm_subscription".freeze,
rejection: "reject_subscription".freeze
},
default_mount_path: "/cable".freeze,
protocols: [
"actioncable-v1-json".freeze,
"actioncable-unsupported".freeze
].freeze
}
# ...
end
# https://github.com/rails/rails/blob/master/actioncable/lib/action_cable/connection/web_socket.rb
module ActionCable
module Connection
# Wrap the real socket to minimize the externally-presented API
class WebSocket # :nodoc:
def initialize(env,
event_target,
event_loop,
protocols: ActionCable::INTERNAL[:protocols]
)
# ...
end
# ...
end
end
end
Проекты бо’льшего масштаба исключением не являются (к сожалению). И если, в примерах выше, это хоть как-то это можно назвать константами, то в реальности разработчики настолько увлекаются, что записывают в константы все что угодно.
Как-то я был на собеседовании в одной компании. В ходе собеседования мы затронули тему “А как ты думаешь, можно ли использовать константы для хранения конфигурации?”. Я тогда однозначно ответил что нет, что это плохо. Спойлер (именно из-за этого мою кандидатуру и отвергли, за что им большое спасибо).
Какие доводы были приведены в пользу использования констант?
Ну в принципе, все так и есть (почти). Но вот контраргументы тоже есть.
RETRIES_COUNT = 10
API_HOST = 'https://your.domain.com'
RETRIES_COUNT = case Rails.env
when 'test'
2
when 'development'
5
when 'staging'
10
else
50
end
API_HOST = case Rails.env
when 'test'
'https://fake.domain.com'
when 'development'
'https://loalhost:3000'
when 'staging'
'https://your-staging.domain.com'
else
'https://your-production.domain.com'
end
class SomeClass
CONSTANT_STRING = "string"
CONSTANT_NUMBER = 1
CONSTANT_ARRAY = [1, 2, 3]
CONSTANT_HASH = { a: :b }
CONSTANT_HELL = {
a: CONSTANT_HASH,
b: CONSTANT_ARRAY,
c: {
d: :e,
f: [1, 2, 3]
}
}
end
[1] pry(main)> SomeClass::CONSTANT_STRING
=> "string"
[2] pry(main)> SomeClass::CONSTANT_STRING << 'smth'
=> "stringsmth"
[3] pry(main)> SomeClass::CONSTANT_STRING
=> "stringsmth"
Всем известно, что тут все просто, используем .freeze и тогда нельзя будет менять никакие значения.
class SomeClass
CONSTANT_STRING = "string".freeze
CONSTANT_NUMBER = 1.freeze
CONSTANT_ARRAY = [1, 2, 3].freeze
CONSTANT_HASH = { a: :b }.freeze
CONSTANT_HELL = {
a: CONSTANT_HASH,
b: CONSTANT_ARRAY,
c: {
d: :e,
f: [1, 2, 3]
}
}.freeze
end
[1] pry(main)> SomeClass::CONSTANT_STRING
=> "string"
[2] pry(main)> SomeClass::CONSTANT_STRING << 'smth'
RuntimeError: can't modify frozen String
[3] pry(main)> SomeClass::CONSTANT_HELL[:a] = 123
RuntimeError: can't modify frozen Hash
[4] pry(main)> SomeClass::CONSTANT_HELL
=> {:a=>{:a=>:b}, :b=>[1, 2, 3], :c=>{:d=>:e, :f=>[1, 2, 3]}}
[5] pry(main)> SomeClass::CONSTANT_HELL[:c][:d] = 123
=> 123
[6] pry(main)> SomeClass::CONSTANT_HELL
=> {:a=>{:a=>:b}, :b=>[1, 2, 3], :c=>{:d=>123, :f=>[1, 2, 3]}}
Только вот freeze не “замораживает” вложенные объекты, и на такую ситуацию можно очень легко нарваться.
Но нужно отдать должное: переопределить константу не получится. Это единственное, пожалуй, чем они хороши.
Если кому-то интересно спросить про то, как это работает внутри ruby машины - поговорим с вами от этом в кулуарах, поэтому пойдем дальше.
Да, если кто-то хочет сказать, что можно при этом использовать переменные окружения - да, можно. Но это также не работает :)
Второй по популярности подход - складывать все параметры в YAML файлы, которые потом будут считаны и полученный хеш будет использоваться как объект хранения конфигурации.
Плюсы у этого подхода, несомненно есть. Как и минусы, в прочем…
common: &common
adapter: postgresql
host: <%= ENV.fetch('DB_HOST') %>
database: <%= ENV.fetch('DB_NAME') %>
username: <%= ENV.fetch('DB_USERNAME') %>
password: <%= ENV.fetch('DB_PASSWORD') %>
encoding: unicode
pool: <%= ENV.fetch('DB_POOL') %>
development:
<<: *common
test:
<<: *common
staging:
<<: *common
port: 5432
cable.yml
? За что его обделили? $ grep -R ENV config app lib | wc -l
458
С последним пунктом я познакомился довольно давно, когда контрибьютил в Gitlab. Там было 3 основных проблемы:
config.yml.example
), который нужно было скопировать и переписать. Как только в config.yml.example
появляются новые параметры, которые вы, в силу какие-то обстоятельств, не заметили, у вас могут начаться проблемы.Как решились эти проблемы? Да никак. Ну то есть мы у себя решили их ;) но Undev
больше нет и никто уже про это не вспоминает. Я, по крайней мере, надеюсь на это.
Что ж, на текущий момент этот подход лидирует в сообществе rails (и не только) разработчиков.
Про этот вариант наверное стоило сказать раньше, так как он появился и прижился раньше, чем вариант с файлами конфигурации. Но я его все же поставил следующим пунктом, так как он все же может использоваться в более гибком варианте.
Я думаю, что все знают про
Rails.application.configure do
# ...
end
Так вот этот вариант как раз из этой оперы.
Плюсы:
Мне в частности, еще нравится вариант Dry-configurable
(я вообще поклонник Dry)
class App
extend Dry::Configurable
# Pass a block for nested configuration (works to any depth)
setting :database do
# Can pass a default value
setting :dsn, 'sqlite:memory'
end
# Defaults to nil if no default value is given
setting :adapter
# Pre-process values
setting(:path, 'test') { |value| Pathname(value) }
# Passing the reader option as true will create attr_reader method for the class
setting :pool, 5, reader: true
# Passing the reader attributes works with nested configuration
setting :uploader, reader: true do
setting :bucket, 'dev'
end
end
App.config.database.dsn
# => "sqlite:memory"
App.configure do |config|
config.database.dsn = 'jdbc:sqlite:memory'
end
App.config.database.dsn
# => "jdbc:sqlite:memory"
App.config.adapter
# => nil
App.config.path
# => #<Pathname:test>
App.pool
# => 5
App.uploader.bucket
# => 'dev'
Также довольно крепко прижился подобный вариант хранения конфигурации и в рубишном мире (в том числе и в самом коде Rails).
Суть в этом подходе сводится к следующему: данные конфигурации считываются из какого-то источника (источников) и сохраняются в одном объекте.
Плюсы и минусы зависят от того, откуда и как берутся эти данные.
Я всегда вспоминаю времена работы в web студии, когда клепал стайтики на CMS’ках, когда вижу подобный подход. Когда есть табличка в базе, там в виде key => value хранятся параметры конфигурирование и разработчик из кода бегает в эту базу для того, чтобы считывать те или иные значения. На самом деле, отвратительно плохого в этом ничего нет. Это один из способов расшарить конфигурацию, которая может поменяться в любой момент. И не важно, где она будет (PostgreSQL, Redis, etc)
Проблемы:
Плюсы:
Очевидно, что чем сложнее приложение, тем больше библиотек используется. И зачастую, каждая библиотека просит свой собственный конфигурационный файл. Если говорить в контексте Rails, это приводит к тому, что 2 директории начинают распухать от количества подобных файлов:
config
config/initializers
Проблема тут, как понимаете, не в количестве файлов, а в количестве переменных/модулей/классов, которые хранят значения конфигурации. Так или иначе, рост количества таких сущностей приводит к тому, что появляются дополнительные абстракции, для того, чтобы соблюсти принципы DRY, что приводит к большой связанности кода и меньшему пониманию, что происходит. И начинается проявляться дополнительная проблема, как обеспечить консистентность конфигурации. Проблема поддержки. Однако, эта проблема легко перекладывается на плечи других людей (например DevOps, мы же говорили о том, что можно все эти проблемы решить при помощи переменных окружения, хотя и это тоже бред, как минимум потому что в конце концов невозможно придумывать и совмещать большое количество переменных)
С подобной ситуацией сталкивается не так много команд и проектов, однако такое бывает и стоит про это сказать. Как много людей используют в своих решениях не только ruby библиотеки а конкретный софт, который имеет вполне конкретную конфигурацию? Что, если есть небольшие сервисы, которые имеют свои конфиги? Вам же при этом (не всегда) нужно учитывать параметры из этих конфигов. Копипастить? Прокидывать через переменные окружения? А что делать, если сервис запускается супервизиром при старте системы? Жонглировать параметрами окружения? А если сервис не может считать эти параметры окружения (умеет читать только из файла)? Переписывать сервис только для того, чтобы он смог это сделать? А если вы не знаете язык программирования или ПО проприетарное? Все эти вопросы в итоге сводятся к тому, что нужно просто продублировать важные параметры. Просто продублировать, вместо того, чтобы переиспользовать. Да, я не спорю, этот вопрос также можно решить при помощи DevOps’ов. Да и то, не всегда. В общем, иногда возникает ситуация, когда нужно просто считать еще один конфиг и иметь к нему доступ из кода приложения.
Как часто вам нужно из нескольких параметров конфигурации собрать 1? Далеко ходить не нужно - строка подключения к базе данных. Вспомним про конфиг базы данных.
common: &common
adapter: postgresql
host: <%= ENV.fetch('DB_HOST') %>
database: <%= ENV.fetch('DB_NAME') %>
username: <%= ENV.fetch('DB_USERNAME') %>
password: <%= ENV.fetch('DB_PASSWORD') %>
encoding: unicode
pool: <%= ENV.fetch('DB_POOL') %>
development:
<<: *common
test:
<<: *common
staging:
<<: *common
port: 5432
И вам в приложении нужно использовать не только данные в поэтом формате, но и строку подключения одной строкой. Что в таком случае делать?
common: &common
adapter: postgresql
host: <%= ENV.fetch('DB_HOST') %>
database: <%= ENV.fetch('DB_NAME') %>
username: <%= ENV.fetch('DB_NAME') %>
password: <%= ENV.fetch('DB_PASSWORD') %>
encoding: unicode
pool: <%= ENV.fetch('DB_POOL') %>
dsn: postgres://<%= ENV.fetch('DB_NAME') %>:<%= ENV.fetch('DB_PASSWORD') %>@<%= ENV.fetch('DB_HOST') %>:<%= ENV.fetch('DB_PORT') %>/<%= ENV.fetch('DB_NAME') %>?pool=<%= ENV.fetch('DB_POOL') %>
Никто не видит в этом проблему? На самом деле проблемы нет, есть досадная неприятность в виде необходимости контролировать 2 места использования переменной окружения вместо одного.
Но вот если наоборот, из 1 параметра вычленить несколько аттрибутов? У вас на входе dsn
а нужно разбить на отдельные параметры (например строка подключения к AWS RDS DB)?
common: &common
dsn: <%= ENV.fetch(‘DB_DSN’) %>
adapter: ???
host: ???
database: ???
username: ???
password: ???
encoding: unicode
pool: ???
Или дополнить 1 параметр дополнительным данными (например несколько баз данных у редиса)? Вот тут геморроя побольше прибавляется.
Смысла забивать область переменных окружений по каждому чиху я особо не вижу. Достаточно затребовать поддержки подобного поведения/фич у той части проекта, которая отвечает за конфигурацию.
А что делать, когда у вас есть конфигурация, описанная при помощи DSL, конфигурация, которая лежит в базе данных, файлики с конфигами, конфиги, переданные через переменные окружения? Перевести все на один формат? Например использовать только YAML файлы или только DSL? Вообще-то это правильная мысль и я бы выбрал второй вариант. Как минимум в этом случае вы можете использовать lambda
для того, чтобы включить один конфиг во второй.
Это также интересный момент. Давайте представим ситуацию: вы пишете небольшое приложение, которые берет данные из API, что-то с ними делает, результат записывает файл и загружает его в AWS S3 бакет. В этой абстрактной задачке можно выделить 3 компонента:
Очевидно, что каждая из этих частей (как минимум 2) будут требовать credentials. И скорее всего, они будут лежать в 2 файликах рядышком и первая и третья компонента прекрасно их считает и все будет работать. Теперь вопрос: какова вероятность того, что вам потребуется во второй компоненте учитывать конфигурационнные параметры из 1 и 3?
Тут возникают следующие вопросы:
Конечно, это же руби, вы можете вызывать приватные методы и дотянуться до самих пикантных мест, рискуя наткнуться на то, что ваш код сломается, когда изменится внутренний интерфейс библиотеки.
Или проще объединить эти 2 конфига (сделать proxy интерфейс к ним).
А теперь представим, что у нас есть некое описание моделей, с которыми может работать ваши сервисы, и вы вычленили их в отдельный гем. Так или иначе, этот гем будет иметь конфигурацию, и вам может понадобиться доступ к параметрам каких-то ключей из вашего сервиса. Как поступать в таком случае? Дублировать конфиги? Или переиспользовать объект конфигурации из родительского класса?
А если вы при этом подключили еще 1 библиотеку, и там есть свой конфиг? Обращаться напрямую к этому конфигу или тоже, пробросить доступ к нему через ваш конфиг? Если выбираете первый вариант, вам не кажется что это похоже на историю с размазывание констант? И Как быть с правилом одного уровня абстракции? И как гарантировать, что ваш интерфейс конфигурации будет неизменным (подмена бекенда не призывает к изменению кода)?
Вернемся ненадолго к монолитам. Когда у вас приложение выполнено в виде монолита у него есть как плюсы, так и минусы в плане возможности использовать такой подход к обновлению конфигурации. Стоит начать с того, что практического смысла в перезагрузке конфигурации без перезапуска приложения особо то и нет. Как минимум потому, что в самом проекте изменения (обычно) происходят довольно быстро и вероятность того, что изменение конфигурации может придти с изменением кода очень велика. Да и изменение конфигурации в этом случае приравнивается к изменению кода, ввиду того, что связность кода очень велика.
Ситуация немного меняется, когда монолит запускается не совсем как монолит. Несмотря на то, что код и лежит в одном месте, но запускаются все эти компоненты отдельно. Например, отдельно API (да и сам API может также разделяться, например по версиям), Отдельно web часть лично кабинета, отдельно публичные контроллеры, отдельно фоновые задачи critical
уровня и так далее.
И все кардинально изменяется, когда вместо 1-го большого приложения у вас становится много маленьких (или как люди любят это называть - микросервисы).
В последних 2-х случаях законным становится вопрос: Имеет ли практический смысл перезапуск всех сервисов, если код в них не поменялся, но произошли изменения в конфигурации?
Конечно, этот вопрос имеет еще много других параметров:
Очевидно, что чем меньше сервис, тем легковеснее его перезапуск. И даунтайм здесь может быть довольно мал. Но что из себя представляет этот перезапуск? Graceful restart или SIGKIL + SIGEXIT? Если у вас второй вариант - перезапуск всех сервисов при необходимости перезагрузить конфигурацию становится опасной затеей. Как минимум потому что какой-то сервис может
Уже этих пунктов достаточно для того, чтобы усомниться в полезности перезапускать все сервисы при изменении в одном.
Желание держать все сервисы запущенными “на максимально свежем коде” присущи многим разработчикам. Иногда до состояния фанатизма. Хм. А все из вас обновляют гемы каждый день (для того, чтобы гарантировать, что используется самая свежая версия гема)? А почему тогда нужно перезапускать сервис, если изменений в нем никаких не произошло (или изменения в соседнем сервисе не затрагивают изменения в каком-то конкретном)?
Таким образом для меня приоритетнее вопрос: А нужно ли его перезапускать? и Затрагивают ли изменения конфигурации/кода работу самого приложения?
Давайте поподробнее разберем второй момент:
Хорошим тоном является наличие изолированных хранилищ у каждого микросервиса. Под хранилищем можно понимать, например, базу данных. И все данные прокидываются между этими сервисами по сети. Но на практике это практически не достижимо (как минимум сразу). Так или иначе, в самом начале у вас будет, например, пошарена база данных между этими сервисами. Не обязательно, между всеми. Или вам нужно использовать отдельное хранилище для коммуникаций (результат работы одного сервиса использовать в другом, при этом объем данных может быть весьма большим (У нас было такое, что несколько миллионов строк из одного сервиса нужно передать в другой). В таком случае получается, что изменение, например, части конфигурации (информации о местонахождении подобного хранилища) затрагивает не все сервисы системы, а только ограниченное число этих сервисов, соответственно, эти изменения не должны влиять на другие.
В таком случае, нужна возможность сообщить приложению о том, что конфигурация устарела и нужно ее обновить. Как это правильно сделать?
Почему-то чаще всего предлагают реализовать API на стороне каждого сервиса, в которое можно постучать и сообщить о том, что произошло какое-то изменение. Ну… Так себе решение. Как-то лениво реализовывать подобный интерфейс для каждого сервиса самостоятельно.
Идея сводится к тому, чтобы послать SIGHUP сигнал сервису, для того, чтобы он обновил конфигурацию (перечитал). Подобный подход пока что еще имеет широкого применения в мире Ruby. Но это логично, так как все привыкли перезапускать приложение, если что-то поменялось.
Давайте представим такую ситуацию: Мы умеем перезагружать конфигурацию в реальном времени. В таком случае можем получить ситуацию, когда код был запущен на одной конфигурации, произошло её обновление и он начал работать на основании другой конфигурации. С одной стороны, это может быть не страшным, с другой…может быть и полезным. Например вы писали часть данных в одно место, тут прилетела информация о том, что нужно писать в другое место и вы решили переключиться. Выглядит это как-то не очень хорошо. Часть данных в одном месте, часть данных в другом и не известно где и сколько. Или у вас было ограничение на длину строки. Было 1000
символов, стало 800
. Часть сгенерированных строк будут соответствовать требуемому результату, часть нет. Хорошо это или плохо? Я не могу сказать однозначно. Для нас подобные ситуации были и полезными и вредными. Но лучше, когда результат соответствует конкретному критерию. Либо все 1000
символов, либо все 800
.
Соответственно, лучше как-то это держать под контролем. Как много людей использует config object
паттерн в своей работе?
Прокидываем в класс/метод объект конфигурации и используем этот снепшот. За время выполнения операции конфиг может поменяться, снепшот остается неизменным. Если изменения конфигурации были критичными - выполнение завалится и сервис восстановится с новой конфигурацией, продолжит работу и все будет хорошо.
def generate_ad_headline(headline_parts: [])
headline = headline_parts.each_pair.each_with_object({}) do |(ad_id, parts), acc|
acc[ad_id] = parts.each_with_object("") do |part, result_headline|
if (result_headline + part).size < YourApp.config.ads.headline.max_size
result_headline << part
end
end
end
end
def generate_ad_headline(headline_parts: [], config_object: YourApp.config.ads.headline)
headline = headline_parts.each_pair.each_with_object({}) do |(ad_id, parts), acc|
acc[ad_id] = parts.each_with_object("") do |part, result_headline|
if (result_headline + part).size < config.max_size
result_headline << part
end
end
end
end
Consul config etcd подобные сервисы Проблема: у вас есть несколько сервисов
Гибкий код с минимальным набором изменений.
Не завязывайтесь на конкретный сервис (интерфейс гема) или класс, общающийся с внешним миром, напрямую. Используйте сервис-локатеры.
Пример реализации с использование Dry-container
class ServiceLocator
class Container
include Dry::Container::Mixin
end
class << self
attr_reader :instance
def configure
container = Container.new
yield(container)
@instance = new(Rails.application, container)
freeze
end
def [](name)
instance[name]
end
end
attr_reader :app, :container
def initialize(app, container)
@app = app
@container = container
end
def [](name)
container[name]
end
end
ServiceLocator.configure do |container|
load_environment = begin
if ENV['PRODUCTION_SERVICE_LOCATOR']
:prod
elsif Rails.env.test? || Rails.env.development?
:fake
else
:prod
end
end
case load_environment
when :fake
container.register(:sms_gateway, lambda do
Test::Nexmo::Client.new(
key: PactApi.config.nexmo.api_key,
secret: PactApi.config.nexmo.api_secret
)
end)
container.register(:mailgun_client, lambda do
Test::Mailgun::Client.new(PactApi.config.mailgun.api_key)
end)
else
container.register(:sms_gateway, lambda do
Nexmo::Client.new(
key: PactApi.config.nexmo.api_key,
secret: PactApi.config.nexmo.api_secret
)
end)
container.register(:mailgun_client, lambda do
Mailgun::Client.new(PactApi.config.mailgun.api_key)
end)
end
end
email_message.take_to_send!
mg_client = ServiceLocator[:mailgun_client]
begin
response = mg_client.send_message PactApi.config.mailgun.domain, payload
if response.code == 200
mailgun_message_id = response.to_h['id']
mailgun_message_id.gsub!(/[<>]/, '')
email_message.update(external_id: mailgun_message_id)
email_message.sent!
else
email_message.fail_send!
end
rescue
email_message.fail_send!
end
При реализации старайтесь явно реализовать true-way процесса.