Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configure adapters on require #502

Merged
merged 14 commits into from
Mar 19, 2021
Merged

Configure adapters on require #502

merged 14 commits into from
Mar 19, 2021

Conversation

bkeepers
Copy link
Collaborator

@bkeepers bkeepers commented Mar 10, 2021

For most use cases, a user should just be able to require an adapter and have it configure itself with sensible defaults. They can still override the default configuration, but this will make it just a little easer to get started.

  • active_record
  • mongo
  • redis
  • sequel

These are going to be harder because they need to wrap another adapter. Ideally the would just get the current default, and replace it with a wrapped adapter:

  • dalli
  • active_support_cache_store
  • cloud

Edit: Skipping cache adapters for now.

And overall:

  • Tests for initialization

This is related to #500 for improving the onboarding experience.

@bkeepers
Copy link
Collaborator Author

@jnunemaker per our text conversation, I'm trying to figure out a way to initialize the cache adapters with sensible defaults, and it's a little challenging with the adapters that wrap other adapters, namely the caching-related ones.

Here's a couple ideas to work around this issue:

Configuration for store vs cache

A lot (but not all) of the adapters fall into one of two camps: storage or cache. If Configuration had a concept for this, then it would be trivial for the adapters to register themselves when required. This assumes most people would generally only use one storage adapter and one cache adapter.

If we added #store and #cache options to config:

# flipper-active_record.rb
Flipper.configure do |config|
  config.store { Flipper::ActiveRecord::Adapter.new }
end
# flipper-dalli.rb
Flipper.configure do |config|
  config.cache do |store| # the adapter configured by calling `config.store { }`
    Flipper::Cache::Dali.new(store, Dalli::Client.new('localhost:11211'), 600)
  end
end

The current #default interface could continue to be used unchanged, but if the block accepts an argument, it would pass the store adapter wrapped with the cache adapter, allowing flipper-cloud to configure itself:

# flipper-cloud.rb
Flipper.configure do |config|
  config.default do |adapter| # `store` wrapped with `cache`
    Flipper::Cloud.new(, adapter)
  end
end

If you wanted to get really aggressive with this route, I think it could make sense to rename the adapters that fall into store vs cache:

  1. Flipper::Adapter::* => Flipper::Store::* for storage adapters (AR, Redis, Mongo, Sequel, Moneta, Pstore) to make it clear that they are storage adapters
  2. Flipper::Adapter::*Cahe => Flipper::Cache::* for cache adapters (Dalli, RedisCache, ActiveSupportCacheStore, Memoizable, …) to make it clear that they are cache adapters.
  3. There are a few adapters that just wrap adapters, and I'm not really sure what do do with them (ReadOnly, OperationLogger, Sync, etc)

All adapters would maintain their same interfaces. Any renamed classes could be aliased to their current constant names with deprecation warnings to maintain backward compatibility.

Middleware-ish

The adapters that don't wrap (which I was calling storage adapters above) are really the only ones that are exclusive. It seems like the rest can be nested in whichever way makes sense for the application. So maybe it's not "storage vs cache", but closer to a Rack middleware concept, where there's a single storage adapter, and then middleware that wrap it.

# flipper-active_record.rb
Flipper.configure do |config|
  config.store { Flipper::ActiveRecord::Adapter.new }
end
# flipper-dalli.rb
Flipper.configure do |config|
  config.use do |adapter|
    Flipper::Cache::Dali.new(adapter, Dalli::Client.new('localhost:11211'), 600)
  end
end

The main difference between #use and #cache above is that #use can be called multiple times.

# flipper-cloud.rb
Flipper.configure do |config|
  config.default do |adapter| `store` wrapped with any other adapters configured via `#use`    
    Flipper::Cloud.new(, adapter)
  end
end

Thoughts on this? Any preference? Or any other ideas?

@jnunemaker
Copy link
Collaborator

This is exciting!

I think I'd need to see a few examples to be able to decide on API for this.

Like maybe what each of these wold look like:

  • single adapter
  • single adapter with cache
  • single adapter with multiple levels of cache
  • single adapter with read only
  • cloud with same permutations as above

Random thoughts:

  • I don't think I want to use use as I do like the idea of allowing for rack-like middleware for adapters at some point -- something that could provide pre and post check operations without needing to build a fully fledged adapter.
  • I'm fine with changing namespaces as long as we are backwards compatible for a while.
  • I don't like Store but don't have a better suggestion. Storage feels kind of long but more accurate. Store and Cache are the same length which I like. For some reason Store makes me think store like target or walmart or somewhere you'd buy something more than where you'd store something.
  • If people use more than one level of caching, they could just use default. My only concern is that could be confusing to require two cache adapters and have the last require win. We could make cache build on top of each other (aka each cache adapter is configured automatically and added to caches array), but people would need to require in the correct order then.
  • config.default really just becomes the dsl instance. Makes me wonder about config.dsl { |adapter| ... }. Or something named better than that.

* origin/master:
  Move redis-namespace to gemfile
  Fix ci yaml token usage
  Remove body debugging
  Fix failures due to body change
  Add response body to error to debug
  Set FLIPPER_CLOUD_TOKEN when running examples
  Run examples with GitHub Actions
  Simplify example setup with bundler/setup
  Use `Flipper` instead of `Flipper.instance`
  Use `enable_percentage_of_time`
  Use `Flipper.enabed?`
  Use `enable_percentage_of_actors`
  Use `enable_group`
  Update typo lib/flipper/configuration.rb
  Remove Memory setup from README
  Default to using Memory adapter
@bkeepers bkeepers marked this pull request as ready for review March 18, 2021 15:24
@bkeepers
Copy link
Collaborator Author

@jnunemaker I messed around with a few different incantations of changing how the configuration works, and I don't really like any of them.

So I'm thinking I want to let it simmer for a while, skip default configuration for the cache adapters for now, and maybe make another attempt later. They're advanced usage anyway, so I don't think it's a problem to require manual configuration.

This is ready for your review.

@bkeepers bkeepers mentioned this pull request Mar 18, 2021
7 tasks
Copy link
Collaborator

@jnunemaker jnunemaker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great. Thanks for updating the docs too. The initializers being commented out was the only question I had. Was the intent to show not to do it by default and that you'd need to uncomment for it to work?

adapter = Flipper::Adapters::ActiveRecord.new
flipper = Flipper.new(adapter)
# profit...
# require 'flipper/adapters/active_record'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be commented out?

@bkeepers
Copy link
Collaborator Author

Was the intent to show not to do it by default and that you'd need to uncomment for it to work?

Yes, that was what I was thinking. Thoughts on how to better communicate that?

Add Flipper::Identifier with default flipper_id method
@jnunemaker
Copy link
Collaborator

Maybe just bold the "if you want to customize" part and/or put a comment at the top of the block that says if you want to customize?

@jnunemaker
Copy link
Collaborator

Or a comment that says uncomment this either in the code snippet or before it. I'd like to think it is obvious that you need to uncomment, but you never know. Haha

@bkeepers bkeepers merged commit c1f5c17 into master Mar 19, 2021
@bkeepers bkeepers deleted the set-defaults branch March 19, 2021 12:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants