palkan / anyway_config

Configuration library for Ruby gems and applications
MIT License
778 stars 52 forks source link

Dynamic Loader Per Call #102

Closed rience closed 2 years ago

rience commented 2 years ago

Is your feature request related to a problem? Please describe.

I'd like to have custom loader that will load config from database. However, this can't be cached because this is multi-tenant system where configuration will change from request to request.

Describe the solution you'd like

Would it be possible to provide custom loader which will be called every time config value is requested.

Describe alternatives you've considered

Reload the whole config each request.

palkan commented 2 years ago

As far as I understand, you're looking for something like this:

# initialize config once
config = MultiTenantConfig.new

using_tenant("a") { config.x } # this return the value for tenant "a"
using_tenant("b") { config.x } # this return the value for tenant "b"

Right?

So, here is how I see it. An Anyway::Config instance stores the actual values within the #values Hash and uses #values reader everywhere.

We can make our values tenant-aware:

class MultiTenantConfig < Anyway::Config
  def values
     values_for_tenant(Current.tenant)
  end

  private

  def values_for_tenant(name)
     return tenant_values[name] if tenant_values.key?(name)

     # load new instance of the config in the context of the current tenant
     tenant_config = self.class.new
     # you can return them all, or merge tenant-dependent values with global,
     # or do whatever you want here
     tenant_values[name] = tenant_config.values
  end

  def tenant_values
    @tenant_values ||= {}
  end
end

This way we cache a config per tenant (lazily).

rience commented 2 years ago

Thank you - it does work nicely. I have to make some additional modifications - because I wanted to fallback to other sources (i.e. YAML or default values) if these values can't be found in database so I did the following

def values_for_tenant(tenant)
  # returning cached values for tenant (if exists)
  return tenant_values[tenant.id] if tenant_values.key?(tenant.id)

  # getting all config values for tenant and put them in hash
  configs = # query db
  overrides = configs.to_h do |config|
    [config.key.to_sym, config.value]
  end.to_h

  # caching values for tenant but first merge with general config params (the ones that come from YAML or class defaults)
  tenant_values[tenant.id] = method(:values).super_method.call.merge(overrides)
end

I also had to overwrite clear method. Otherwise it'd cache values for tenants.

def clear
  super
  tenant_values.clear
end

And it does works great. Thank you for your help.