What is a good way to Monkey patch ActiveStorage::Blob to dynamically reset the service configuration

I created a monkey patch to set active storage service depending on settings in the database. (Code of the monkey patch bellow)

It works but it raises error ActiveRecord::RecordNotSaved (Failed to save the new associated _attachment) when I change the storage settings. Works again when I reload the rails server.

Monkey patch code in file lib/core_extensions/active_storage/dynamic_service_configuration.rb

## Dynamically sets Active storage service, depending on settings from the database
def set_config
  storage_type = Setting.find_by_name('active_storage_type')
  configs = {}
  case storage_type
  when 'disk'
    configs[:disk] = {
        service: 'Disk',
        root: Rails.root.join('tmp/storage').to_s
    }
    ActiveStorage::Blob.services = ActiveStorage::Service::Registry.new(configs)
    ActiveStorage::Blob.service = ActiveStorage::Blob.services.fetch(:disk)
  when 's3'
    configs[:s3] = {
        service: 'S3',
        access_key_id: Setting.find_by_name('active_storage_access_key'),
        secret_access_key: Setting.find_by_name('active_storage_secret_key'),
        bucket: Setting.find_by_name('active_storage_bucket'),
        region: Setting.find_by_name('active_storage_region')
    }
    ActiveStorage::Blob.services = ActiveStorage::Service::Registry.new(configs)
    ActiveStorage::Blob.service = ActiveStorage::Blob.services.fetch(:s3)
  else
    raise "Unknown storage type #{storage_type}"
  end
end
# Service is dynamically configurable using the Setting active record model.
# Settings are saved in the database and is customizable from the super admin interface
# Extend the after initialize to change config each time
module CoreExtensions
  module ActiveStorage
    module DynamicServiceConfiguration
      def self.prepended(base)
      set_config
        base.after_initialize do
          set_config
        end
      end
    end
  end
end

# Monkey patch ActiveStorage Blob when loaded
ActiveSupport.on_load(:active_storage_blob) do
  if ::ActiveStorage::Blob.included_modules.exclude?(CoreExtensions::ActiveStorage::DynamicServiceConfiguration)
    ::ActiveStorage::Blob.prepend CoreExtensions::ActiveStorage::DynamicServiceConfiguration
  end
end

Inspired from https://github.com/rails/rails/blob/main/activestorage/lib/active_storage/engine.rb

initializer "active_storage.services"

I don’t know if this is possible, and I’m not sure what the risks are to change config after initialization.

What are you trying to accomplish?

Hi,

I am trying to change the storage service configuration at runtime, without reloading the server based on settings saved in the database.

For instance: → In the Database service is set to be Amazon S3 → Change the Database service setting to be Disk storage (from an admin user interface for example) → I want the next time active storage is used to pick up this new setting without having to reload the rails server

I hope it make sense, I tried looking at active storage documentation to see if there is not already a feature like this implemented but could not find it.

As far as I know, you cannot change configuration after initialization (during runtime) of Rails apps.

I also don’t think it is a good idea to change this configuration without restarting the server. For example, what if there is an upload in progress while this change occurs? The same principle can apply to writes during ActiveRecord transactions.

What is the business value you’re trying to add by being able to switch services?

This doesn’t seem to be a feature or use-case for ActiveStorage, that I know of.

1 Like

@zzak , Thank you for your answer. You are right. Not a good idea. I will go ahead and just make restarting the server a requirement after changing the configuration.

The business value is to give the admin the possibility to configure storage from a UI.

If providing the admin access to open a PR to edit your config/storage.yml is too high of a barrier. One idea would be to create a workflow that reads the configuration from the database and writes to the file during CI/CD – this would also ensure these changes don’t break your application.