{"id":15288805,"url":"https://github.com/rnd-soft/gorynich","last_synced_at":"2025-04-13T06:52:01.720Z","repository":{"id":163988319,"uuid":"639418233","full_name":"RND-SOFT/gorynich","owner":"RND-SOFT","description":"[MIRROR] Multitenancy for Rails including ActiveRecord, ActionCable, ActiveJob and other subsystems","archived":false,"fork":false,"pushed_at":"2024-11-13T12:03:53.000Z","size":185,"stargazers_count":15,"open_issues_count":0,"forks_count":1,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-03-26T23:11:42.311Z","etag":null,"topics":["activerecord","gem","mirror","multitenancy","ruby","ruby-on-rails"],"latest_commit_sha":null,"homepage":"https://br.rnds.pro/ruby/gorynich","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RND-SOFT.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-05-11T12:42:09.000Z","updated_at":"2024-12-25T20:00:24.000Z","dependencies_parsed_at":"2024-10-23T01:04:39.142Z","dependency_job_id":null,"html_url":"https://github.com/RND-SOFT/gorynich","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RND-SOFT%2Fgorynich","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RND-SOFT%2Fgorynich/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RND-SOFT%2Fgorynich/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RND-SOFT%2Fgorynich/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RND-SOFT","download_url":"https://codeload.github.com/RND-SOFT/gorynich/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248675455,"owners_count":21143767,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activerecord","gem","mirror","multitenancy","ruby","ruby-on-rails"],"created_at":"2024-09-30T15:53:16.104Z","updated_at":"2025-04-13T06:52:01.683Z","avatar_url":"https://github.com/RND-SOFT.png","language":"Ruby","readme":"# Gorynich\n\n\u003cdiv align=\"center\"\u003e\n\n[![Gem Version](https://badge.fury.io/rb/gorynich.svg)](https://rubygems.org/gems/gorynich)\n[![Gem](https://img.shields.io/gem/dt/gorynich.svg)](https://rubygems.org/gems/gorynich/versions)\n[![YARD](https://badgen.net/badge/YARD/doc/blue)](http://www.rubydoc.info/gems/gorynich)\n\n\n[![coverage](https://lysander.rnds.pro/api/v1/badges/gorynich_coverage.svg)](https://lysander.rnds.pro/api/v1/badges/gorynich_coverage.html)\n[![quality](https://lysander.rnds.pro/api/v1/badges/gorynich_quality.svg)](https://lysander.rnds.pro/api/v1/badges/gorynich_quality.html)\n[![outdated](https://lysander.rnds.pro/api/v1/badges/gorynich_outdated.svg)](https://lysander.rnds.pro/api/v1/badges/gorynich_outdated.html)\n[![vulnerable](https://lysander.rnds.pro/api/v1/badges/gorynich_vulnerable.svg)](https://lysander.rnds.pro/api/v1/badges/gorynich_vulnerable.html)\n\n\u003c/div\u003e\n\n`Gorynich` - это гем для реализации [мультитенантности](https://ru.wikipedia.org/wiki/Мультиарендность) (мультиарендности) в Ruby on Rails приложении. Позволяет обеспечить строгую изоляцию данных в нескольких СУБД, поддерживаемых в ActiveRecord.\n\nПоскольку мультитенантное приложение тесно связано с разделением данных, которые в свою очередь могут находиться в разных источниках (СУБД, S3, Redis и пр.), а также с их обработкой в разных подсистемах (ActiveJob, ActionCable), мы выбрали название [\"Горыныч\"](https://ru.wikipedia.org/wiki/Змей_Горыныч), чтобы подчеркнуть ~многоголовость~ многогранность интеграций.\n\n---\n\n`Gorynich` provides tools for creating [Multitenancy](https://en.wikipedia.org/wiki/Multitenancy) Ruby on Rails application. If you need to have strong data segregation and isolated DBMS's with diffrent providers (supported by ActiveRecord) and credentials, `Gorynich` can help.\n\nSince a multi-tenant application is closely related to the separation of data, which in turn can be located in different sources (DBMS, S3, Redis, etc.), as well as their processing in different subsystems (ActiveJob, ActionCable), we chose the name [\"Gorynych\"](https://en.wikipedia.org/wiki/Zmei_(Russian)#Multiheadedness), which to emphasize the ~multiheadedness~ versatility of integrations.\n\n\u003cdiv align=\"left\"\u003e\n  \u003ca href=\"https://rnds.pro/\" \u003e\n    \u003cimg src=\"https://library.rnds.pro/repository/public-blob/logo/RNDS.svg\" alt=\"Supported by RNDSOFT\"  height=\"60\"\u003e\n  \u003c/a\u003e\n\u003c/div\u003e\n\n## Возможности / Features\n\n- Прозрачное переключение БД/СУБД на основании данных запросов / Transparent request based DB/DBMS switching\n- Интеграция с / Integrations:\n  - ActiveRecord\n  - ActionCable\n  - ActiveJob\n  - DelayedJob\n- Получение параметров из [Consul KV](https://developer.hashicorp.com/consul/docs/dynamic-app-config/kv) / Stoting configuration in [Consul KV](https://developer.hashicorp.com/consul/docs/dynamic-app-config/kv)\n- Получение параметров из файла / Storing configuration in file\n- Разделение секретов / Secret storing and isolation\n- Обновление конфигурации \"на лету\" / update configuration \"on the fly\"\n- Статическая генерация `database.yml` / Static `database.yml` generation\n\n\n## Начало работы / Getting started\n\n```sh\ngem install gorynich\n```\n\nПри установке `Gorynich` через bundler добавьте следующую строку в `Gemfile`:\n\n---\n\nIf you'd rather install `Gorynichr` using bundler, add a line for it in your `Gemfile`:\n\n```sh\ngem 'gorynich'\n```\n\nЗатем выполните / Then run:\n\n```sh\nbundle install # для установки гема / gem installation\n\nrails generate gorynich:install # для добавления шаблонов конфигурации / install configuration templates\n```\n\n## Что такое тенант? / What tenant is?\n\nТенант (в данном случае) - это активное подключение к СУБД, а также доступный в любом месте объект `Gorynich::Current`, в котором находятся параметры текущего тенанта. К нему можно обратиться в любом месте.\n\n---\n\nIn this case tenant is an active connection to the DBMS, as well as a `Gorynich::Current` object available anywhere, which contains the parameters of the current tenant. You can refer to it anywhere, for example when sending emails:\n\n\n```ruby\nGorynich::Current.tap do |t|\n  t.tenant   # tenant_name\n  t.uri      # https://app.domain.org\n  t.host     # app.domain.org\n  t.secrets  # { key1 =\u003e value1, key2 =\u003e value2}\n  t.database # { adapter =\u003e postgresql, host =\u003e localhost, port =\u003e 5432, username =\u003e xxx, password =\u003e xxx }\nend\n```\n\n## Как это работает / How it works\n\nПеред обработкой запроса с помощью [Gorynich::Rack::RackMiddleware](./lib/gorynich/head/rack_middleware.rb) соединение Active Record переключается на указанную БД, а с помощью [ActiveSupport::CurrentAttributes](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html) в любом месте приложения становятся доступны дополнительные параметры через обращение к `Gorynich::Current`. ActionCable, ActiveJob и другие \"головы\" используют настройки из `Gorynich::Current` для сохранения контекста и дальнейшего исполнения.\n\nНапример, при отправке писем изнутри ActiveJob использование выглядит так:\n\n---\n\nBefore request processing [Gorynich::Rack::RackMiddleware](./lib/gorynich/head/rack_middleware.rb) ActiveRecord connection switching to apropriate database. Additional tenant properties available in any part of application through [ActiveSupport::CurrentAttributes](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html) as  `Gorynich::Current` instance. ActionCable, ActiveJob and other \"heads\" also uses `Gorynich::Current` to store context and evaluate it later.\n\nFor example, when sending emails from within ActiveJob, the usage looks like this:\n\n```ruby\n#app/mailers/application_mailer.rb\n\nclass ApplicationMailer \u003c ActionMailer::Base\n  helper :application\n\n  def self.email_settings\n    (\n      Gorynich::Current.secrets[:email_settings] || Rails.application.secrets.email_settings || {}\n    ).with_indifferent_access\n  end\n\n  default from: email_settings[:from], content_type: 'text/plain'\n\n  def mail(args)\n    @host = Gorynich.instance.hosts(Gorynich::Current.tenant).first || Rails.application.secrets.domain\n\n    @settings ||= smtp_settings.merge(application_host: @host)\n\n    super(args).tap |m|\n      m.from = @settings[:from]\n      m.delivery_method.settings = @settings unless Rails.env.development?\n    end\n  end\n\nend\n```\n\n## Использование / Usage\n\n### Настройка источника данных / Configuration source\n\nДля использования необходимо в файле `config/application.rb` добавить источник данных. Сейчас доступны 3 источника:\n\n---\n\nNow you need to select configuration source in `config/application.rb`. Yuo can choose from 3 source types now:\n\n\n```ruby\nGorynich::Fetchers::File.new(file_path: [FILE_PATH]) # из файла / from file\n\nGorynich::Fetchers::Consul.new(storage: [CONSUL_KEY], **options) # из консула / from consul (options - from Dimplomat gem https://github.com/WeAreFarmGeek/diplomat)\n\nGorynich::Fetchers::ConsulSecure.new(storage: [CONSUL_KEY], file_path: [FILE_PATH], **options) # из консула с сохранением в файл (при недоступности консула будет читать из файла) / from consul with saving to a file (if unavailable, consul will read from the file) (options - from Dimplomat gem https://github.com/WeAreFarmGeek/diplomat)\n```\n\nПример / Example:\n\n```ruby\n# из одного / from single source\nGorynich.configuration.fetcher = Gorynich::Fetchers::File.new(file_path: Rails.root.join('config', 'gorynich_config.yml'))\n\n# из нескольких (данные берутся от первого успешного fetcher)\n# from multiple sources - first succesful source is used\nGorynich.configuration.fetcher  = [\n  Gorynich::Fetchers::Consul.new(storage: 'gorynich_project/config'),\n  Gorynich::Fetchers::File.new(file_path: Rails.root.join('config', 'gorynich_config.yml'))\n]\n```\n\n### Настройка интеграций (\"голов\") / Integration \"Heads\"\n\n`Gorynich` настраивается в обычных инициалайзерах / `Gorynich` configured in initializer:\n\n```ruby\n# config/initializers/gorynich.rb\n\nGorynich.configure do |config|\n  # config cache of gorynich\n  config.cache = Rails.cache\n\n  # config cache namespace\n  config.namespace = ENV.fetch('YOUR_NAMESPACE_ENV', 'your_namespace')\n\n  # config how long your source cache will be alive in seconds\n  config.cache_expiration = 'your_value'\n\n  # Custom handler for swithing tenants in gorynich rack middleware\n  config.rack_env_handler =\n    lambda do |env|\n      host = env['SERVER_NAME']\n      tenant = Gorynich.instance.tenant_by_host(host)\n      uri = Gorynich.instance.uri_by_host(host, tenant)\n\n      Sentry.set_tags(tenant: tenant) if Sentry.get_current_scope.present?\n\n      [tenant, { host: host, uri: uri }]\n    end\nend\n\n# Add cable head\nActiveSupport.on_load(:action_cable_connection) do\n  include Gorynich::Head::ActionCable::Connection\nend\n\nActiveSupport.on_load(:action_cable_channel) do\n  prepend Gorynich::Head::ActionCable::Channel\nend\n\n# Add active job head\nActiveSupport.on_load(:active_job) do\n  include Gorynich::Head::ActiveJob\nend\n```\n\n### Rake Tasks\n\nЗапуск `Rails console` внутри тенанта / Run `rails console` inside tenant:\n\n```bash\nTENANT=tenant rails gc # default tenant name id  'default'\n```\n\nДля создания статичного файла `database.yml` из источника данных (Fetcher) используйте:\n\n---\n\nFor static `database.yml` generation from configured source (Fetcher) use:\n\n```bash\nrails gc:db:prepare\n```\n\n### Настройка конфигурации БД / Database configuration\n\n1. Статическая генерация / static generation\n\nПервый, самый простой способ работы, подходящий для локальной разработки, это статическая генерация `database.yml`.\n\n---\n\nFirst and most simple using of Gorynich handy for local development is static `database.yml` generation.\n\nзапуск rake-задачи / runing rake task:\n\n```bash\nrails gc:db:prepare\n```\n\n2. Полуавтоматический режим / Semi-automated mode\n\nВторой вариант - это создание конфигурации `database.yml` при старте Rails приложения - данные будут прочитаны из настроенного источника. В этом случае конфигурация СУБД может изменяться только при перезапуске приложения, но остальные настройки, такие как привязка тенантов к доменам и secrets, будут подхватываться \"на лету\" непосредственно во время работы приложения. Rake-задачи `db:create`, `db:migrate` работают для всех тенантов на момент запуска.\n\n---\n\nSecond option is dynamic `database.yml` creation while starting Rails application. Configuration will be readed from selected source. In this case database configuration can change only when application restarts, but other configuration such a domain to tenant binding and application secrets wil be updated \"on the fly\" while application running. Rake tasks `db:create` and `db:migrate` works as expected for all tenant in order.\n\n\u003e ВНИМАНИЕ! `db:rollback` не работает в мультитенантном режиме.\n\n\u003e WARNING!  `db:rollback` is not working in multitenancy mode.\n\nВ `database.yml` прописать следующее / In `database.yml` set:\n\n```yaml\n# config/database.yml\n\u003c%= Gorynich.instance.database_config %\u003e\n```\n\n3. Дополнительные БД / Additional databases\n\nЕсли вам нужны дополнительные БД, не являющиеся тенантами, например общая БД, то в `database.yml` можно дописать всё необходимое, как в обычном Rails приложении:\n\n---\n\nIf you need additional DB, not for tenants Ex. generic database you can configure it in `database.yml` like in regular Rails application:\n\n```yaml\n# config/database.yml\n\u003c%= Gorynich.instance.database_config('development') %\u003e\n\nyour_database:\n  \u003c\u003c: *configs_for_your_database\n\n\u003c%= Gorynich.instance.database_config('test') %\u003e\nyour_database:\n  \u003c\u003c: *configs_for_your_database\n\n\u003c%= Gorynich.instance.database_config('production') %\u003e\nyour_database:\n  \u003c\u003c: *configs_for_your_database\n```\n\n### В коде / Inside code\nПроверить, в каком вы тенанте, можно с помощью / Check in which tenant you are:\n\n```ruby\nGorynich::Current.tenant\n```\n\nПереключение тенантов работает автоматически, и внутри Rails приложения не нужно предпринимать никаких дополнительных действий - вы всегда подключены к той базе данных, к которой привязан домен текущего запроса (или иной параметр). Но если необходимо явно выполнить действия в контексте конкретного тената, это можно сделать:\n\n---\n\nSwitching tenants is automatic and no additional steps need to be taken inside a Rails application - you always connected to database associated with currently procesed request. But if you want take action inside specific tenant context you can use:\n\n```ruby\n  # для выполнения в конкретном тенанте / run block inside specific tenant\n  Gorynich.with('tenant_name') do\n    # your code\n  end\n\n  # для выполнения в каждом тенанте / run block inside each tenant\n  Gorynich.with_each_tenant do |tenant|\n    # your code\n  end\n```\n\n## Примеры дополнительных интеграций и использований / Additional integration examples\n\n### Redis / Rails.cache\n\n```ruby\n#config/environments/production.rb\n\nconfig.cache_store = :redis_cache_store, {\n  url:                ENV.fetch('REDIS_URL', nil),\n  expires_in:         90.minutes,\n  connect_timeout:    3,\n  reconnect_attempts: 3,\n  namespace:          -\u003e { \"#{Gorynich.configuration.namespace}#{Gorynich::Current.tenant}\" }\n}\n```\n\n### Sentry\n\n```ruby\n#config/initializers/gorynich.rb\n\nGorynich.configure do |config|\n  config.rack_env_handler =\n    lambda do |env|\n      host = env['SERVER_NAME']\n      tenant = Gorynich.instance.tenant_by_host(host)\n      uri = Gorynich.instance.uri_by_host(host, tenant)\n\n      Sentry.set_tags(tenant: tenant) if Sentry.get_current_scope.present?\n\n      [tenant, { host: host, uri: uri }]\n    end\nend\n```\n\n### Telegram\n\n```ruby\n#config/environments/production.rb\n\nconfig.telegram_updates_controller.session_store = :redis_cache_store, {\n  url:        ENV.fetch('REDIS_URL', nil),\n  expires_in: 90.minutes,\n  namespace:  -\u003e { \"#{Gorynich.configuration.namespace}#{Gorynich::Current.tenant}\" }\n}\n```\n\n### Shrine\n\n```ruby\n#lib/shrine/plugins/tenant_location.rb\n\nclass Shrine\n  module Plugins\n    module TenantLocation\n      module InstanceMethods\n        def generate_location(io, **options)\n          \"#{Gorynich::Current.tenant}/#{super}\"\n        end\n      end\n    end\n\n    register_plugin(:tenant_location, TenantLocation)\n  end\nend\n\n#config/initializers/shrine.rb\n\nShrine.plugin :tenant_location\n```\n\n### ApplicationController\n\n```ruby\nclass ApplicationController \u003c ActionController::Base\n  around_action :around_action_notification\n\n  def around_action_notification(\u0026block)\n    ActiveSupport::Notifications.instrument(\n      'around_action.action_controller',\n      current_user: current_user,\n      request:      request,\n      tenant:       Gorynich::Current.tenant, \u0026block\n    )\n  end\nend\n```\n\n### DelayedJob\n\n```ruby\n#config/initializers/delayed_job.rb\n\nrequire 'gorynich/head/delayed_job'\n\nDelayed::Worker.plugins \u003c\u003c Gorynich::Head::DelayedJob\n```\n\n## Лицензия / License\n\nБиблиотека доступна с открытым исходным кодом в соответствии с условиями [лицензии MIT](./LICENSE).\n\n---\n\nThe gem is available as open source under the terms of the [MIT License](./LICENSE).\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frnd-soft%2Fgorynich","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frnd-soft%2Fgorynich","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frnd-soft%2Fgorynich/lists"}