Giter Site home page Giter Site logo

tompave / fun_with_flags Goto Github PK

View Code? Open in Web Editor NEW
1.1K 8.0 79.0 813 KB

Feature Flags/Toggles for Elixir

Home Page: https://hexdocs.pm/fun_with_flags/FunWithFlags.html

License: MIT License

Elixir 99.84% Shell 0.16%
elixir feature-toggles feature-flags redis ecto phoenix-framework

fun_with_flags's Introduction

FunWithFlags

Mix Tests Code Quality
Hex.pm hexdocs.pm Hex.pm Downloads License ElixirWeekly ElixirCasts

FunWithFlags, the Elixir feature flag library.

If you're reading this on the GitHub repo, keep in mind that this readme refers to the master branch. For the latest version released on Hex, please check the readme published with the docs.


FunWithFlags is an OTP application that provides a 2-level storage to save and retrieve feature flags, an Elixir API to toggle and query them, and a web dashboard as control panel.

It stores flag information in Redis or a relational DB (PostgreSQL, MySQL, or SQLite - with Ecto) for persistence and synchronization across different nodes, but it also maintains a local cache in an ETS table for fast lookups. When flags are added or toggled on a node, the other nodes are notified via PubSub and reload their local ETS caches.

Content

What's a Feature Flag?

Feature flags, or feature toggles, are boolean values associated to a name. They should be used to control whether some application feature is enabled or disabled, and they are meant to be modified at runtime while an application is running. This is usually done by the people who control the application.

In their simplest form, flags can be toggled on and off globally. More advanced rules or "gates" allow a fine grained control over their status. For example, it's possible to toggle a flag on and off for specific entities or for groups.

The goal is to have more granular and precise control over what is made available to which users, and when. A common use case, in web applications, is to enable a functionality without the need to deploy or restart the server, or to enable it only for internal users to test it before rolling it out to everyone. Another scenario is the ability to quickly disable a functionality if it's causing problems. They can also be used to implement a simple authorization system, for example to an admin area.

Usage

FunWithFlags has a simple API to query and toggle feature flags. Most of the time, you'll call FunWithFlags.enabled?/2 with the name of the flag and optional arguments.

Different kinds of toggle gates are supported:

  • Boolean: globally on and off.
  • Actors: on or off for specific structs or data. The FunWithFlags.Actor protocol can be implemented for types and structs that should have specific rules. For example, in web applications it's common to use a %User{} struct or equivalent as an actor, or perhaps the current country of the request.
  • Groups: on or off for structs or data that belong to a category or satisfy a condition. The FunWithFlags.Group protocol can be implemented for types and structs that belong to groups for which a feature flag can be enabled or disabled. For example, one could implement the protocol for a %User{} struct to identify administrators.
  • %-of-Time: globally on for a percentage of the time. It ignores actors and groups. Mutually exclusive with the %-of-actors gate.
  • %-of-Actors: globally on for a percentage of the actors. It only applies when the flag is checked with a specific actor and is ignored when the flag is checked without actor arguments. Mutually exclusive with the %-of-time gate.

Boolean, Actor and Group gates can express either an enabled or disabled state. The percentage gates can only express an enabled state, as disabling something for a percentage of time or actors is logically equivalent to enabling it for the complementary percentage.

Gate Priority and Interactions

The priority order is from most to least specific: Actors > Groups > Boolean > Percentage, and it applies to both enabled and disabled gates.

For example, a disabled group gate takes precedence over an enabled boolean (global) gate for the entities in the group, and a further enabled actor gate overrides the disabled group gate for a specific entity. When an entity belongs to multiple groups with conflicting toggle status, the disabled group gates have precedence over the enabled ones. The percentage gates are checked last, if present, and they're only checked if no other gate is enabled.

As another example, a flag can have a disabled boolean gate and a 50% enabled %-of-actors gate. When the flag is checked with an actor, it has a (deterministic, consistent and repeatable) 50% chance to be enabled, but when checked without an actor argument it will always be disabled. If we add to the flag a disabled actor gate and an enabled group gate, the flag will be always disabled for the actor, always enabled for any other actor matching the group, have a 50% change to be enabled for any other actor, and always be disabled when checked without actor arguments. If, then, we replace the 50%-of-actors gate with a 50%-of-time gate, the flag will be always disabled for the actor, always enabled for any other actor matching the group, and have a 50% chance to be enabled for any other actor or when checked without an actor argument.

Boolean Gate

The boolean gate is the simplest one. It's either enabled or disabled, globally. It's also the gate with the second lowest priority (it can mask the percentage gates). If a flag is undefined, it defaults to be globally disabled.

FunWithFlags.enabled?(:cool_new_feature)
false

{:ok, true} = FunWithFlags.enable(:cool_new_feature)

FunWithFlags.enabled?(:cool_new_feature)
true

{:ok, false} = FunWithFlags.disable(:cool_new_feature)

FunWithFlags.enabled?(:cool_new_feature)
false

Actor Gate

This allows you to enable or disable a flag for one or more entities. For example, in web applications it's common to use a %User{} struct or equivalent as an actor, or perhaps the data used to represent the current country for an HTTP request. This can be useful to showcase a work-in-progress feature to someone, to gradually rollout a functionality by country, or to dynamically disable some features in some contexts.

Actor gates take precedence over the others, both when they're enabled and when they're disabled. They can be considered as toggle overrides.

In order to be used as an actor, an entity must implement the FunWithFlags.Actor protocol. This can be implemented for custom structs or literally any other type.

defmodule MyApp.User do
  defstruct [:id, :name]
end

defimpl FunWithFlags.Actor, for: MyApp.User do
  def id(%{id: id}) do
    "user:#{id}"
  end
end

bruce = %MyApp.User{id: 1, name: "Bruce"}
alfred = %MyApp.User{id: 2, name: "Alfred"}

FunWithFlags.Actor.id(bruce)
"user:1"
FunWithFlags.Actor.id(alfred)
"user:2"

defimpl FunWithFlags.Actor, for: Map do
  def id(%{actor_id: actor_id}) do
    "map:#{actor_id}"
  end

  def id(map) do
    map
    |> inspect()
    |> (&:crypto.hash(:md5, &1)).()
    |> Base.encode16
    |> (&"map:#{&1}").()
  end
end

FunWithFlags.Actor.id(%{actor_id: "bar"})
"map:bar"
FunWithFlags.Actor.id(%{foo: "bar"})
"map:E0BB5BA6873E3AC34B0B6928190C1F2B"

With the protocol implemented, actors can be used with the library functions:

{:ok, true} = FunWithFlags.enable(:restful_nights)
{:ok, false} = FunWithFlags.disable(:restful_nights, for_actor: bruce)
{:ok, true} = FunWithFlags.enable(:batmobile, for_actor: bruce)

FunWithFlags.enabled?(:restful_nights)
true
FunWithFlags.enabled?(:batmobile)
false

FunWithFlags.enabled?(:restful_nights, for: alfred)
true
FunWithFlags.enabled?(:batmobile, for: alfred)
false

FunWithFlags.enabled?(:restful_nights, for: bruce)
false
FunWithFlags.enabled?(:batmobile, for: bruce)
true

Actor identifiers must be globally unique binaries. Since supporting multiple kinds of actors is a common requirement, all the examples use the common technique of namespacing the IDs:

defimpl FunWithFlags.Actor, for: MyApp.User do
  def id(user) do
    "user:#{user.id}"
  end
end

defimpl FunWithFlags.Actor, for: MyApp.Country do
  def id(country) do
    "country:#{country.iso3166}"
  end
end

Group Gate

Group gates are similar to actor gates, but they apply to a category of entities rather than specific ones. They can be toggled on or off for the name of the group instead of a specific term.

Group gates take precedence over boolean gates but are overridden by actor gates.

Group names can be binaries or atoms. Atoms are supported for retro-compatibility with versions <= 0.9 and binaries are therefore preferred. In fact, atoms are internally converted to binaries and are then stored and later retrieved as binaries.

The semantics to determine which entities belong to which groups are application specific. Entities could have an explicit list of groups they belong to, or the groups could be abstract and inferred from some other attribute. For example, an :employee group could comprise all %User{} structs with an email address matching the company domain, or an :admin group could be made of all users with %User{admin: true}.

In order to be affected by a group gate, an entity should implement the FunWithFlags.Group protocol. The protocol automatically falls back to a default Any implementation, which states that any entity belongs to no group at all. This makes it possible to safely use "normal" actors when querying group gates, and to implement the protocol only for structs and types for which it matters.

The protocol can be implemented for custom structs or literally any other type.

defmodule MyApp.User do
  defstruct [:email, admin: false, groups: []]
end

defimpl FunWithFlags.Group, for: MyApp.User do
  def in?(%{email: email}, "employee"), do: Regex.match?(~r/@mycompany.com$/, email)
  def in?(%{admin: is_admin}, "admin"), do: !!is_admin
  def in?(%{groups: list}, group_name), do: group_name in list
end

elisabeth = %MyApp.User{email: "[email protected]", admin: true, groups: ["engineering", "product"]}
FunWithFlags.Group.in?(elisabeth, "employee")
true
FunWithFlags.Group.in?(elisabeth, "admin")
true
FunWithFlags.Group.in?(elisabeth, "engineering")
true
FunWithFlags.Group.in?(elisabeth, "marketing")
false

defimpl FunWithFlags.Group, for: Map do
  def in?(%{group: group_name}, group_name), do: true
  def in?(_, _), do: false
end

FunWithFlags.Group.in?(%{group: "dumb_tests"}, "dumb_tests")
true

With the protocol implemented, actors can be used with the library functions:

FunWithFlags.disable(:database_access)
FunWithFlags.enable(:database_access, for_group: "engineering")

FunWithFlags.enabled?(:database_access)
false
FunWithFlags.enabled?(:database_access, for: elisabeth)
true

Percentage of Time Gate

%-of-time gates are similar to boolean gates, but they allow to enable a flag for a percentage of the time. In practical terms, this means that a percentage of the enabled?() calls for a flag will return true, regardless of the presence of an actor argument.

When a %-of-time gate is checked a pseudo-random number is generated and compared with the percentage value of the gate. If the result of the random roll is lower than the gate's percentage value, the gate is considered enabled. So, at the risk of stating the obvious and for the sake of clarity, a 90% gate is enabled more often than a 10% gate.

%-of-time gates are useful to gradually introduce alternative code paths that either have the same effects of the old ones, or don't have effects visible to the users. This last point is important, because with a %-of-time gate the application will behave differently on a pseudo-random basis.

A good use case for %-of-time gates is to safely test the correctness or performance and load characteristics of an alternative implementation of a functionality.

For example:

FunWithFlags.clear(:alternative_implementation)
FunWithFlags.enable(:alternative_implementation, for_percentage_of: {:time, 0.05})

def foo(bar) do
  if FunWithFlags.enabled?(:alternative_implementation) do
    new_foo(bar)
  else
    old_foo(bar)
  end
end

The %-of-time gate is incompatible and mutually exclusive with the %-of-actors gate, and it replaces it when it gets set. While there are ways to make them work together, it would needlessly overcomplicate the priority rules.

Percentage of Actors Gate

%-of-actors gates are similar to the %-of-time gates, but instead of using a pseudo-random chance they calculate the actor scores using a deterministic, consistent and repeatable function that factors in the flag name. At a high level:

actor
|> FunWithFlags.Actor.id()
|> sha256_hash(flag_name)
|> hash_to_percentage()

Since the scores depend on both the actor ID and the flag name, they're guaranteed to always be the same for each actor-flag combination. At the same time, the same actor will have different scores for different flags, and each flag will have a uniform distribution of scores for all the actors.

Just like for the %-of-time gates, an actor's score is compared with the gate's percentage value and, if lower, the gate will result enabled.

A practical example, based on the FunWithFlags.Actor protocol set up from the previous sections:

defmodule MyApp.User do
  defstruct [:id, :name]
end

defimpl FunWithFlags.Actor, for: MyApp.User do
  def id(%{id: id}) do
    "user:#{id}"
  end
end

frodo  = %MyApp.User{id: 1, name: "Frodo Baggins"}
sam    = %MyApp.User{id: 2, name: "Samwise Gamgee"}
pippin = %MyApp.User{id: 3, name: "Peregrin Took"}
merry  = %MyApp.User{id: 4, name: "Meriadoc Brandybuck"}

FunWithFlags.Actor.Percentage.score(frodo, :pipeweed)
0.8658294677734375
FunWithFlags.Actor.Percentage.score(sam, :pipeweed)
0.68426513671875
FunWithFlags.Actor.Percentage.score(pippin, :pipeweed)
0.510528564453125
FunWithFlags.Actor.Percentage.score(merry, :pipeweed)
0.2617645263671875

{:ok, true} = FunWithFlags.enable(:pipeweed, for_percentage_of: {:actors, 0.60})

FunWithFlags.enabled?(:pipeweed, for: frodo)
false
FunWithFlags.enabled?(:pipeweed, for: sam)
false
FunWithFlags.enabled?(:pipeweed, for: pippin)
true
FunWithFlags.enabled?(:pipeweed, for: merry)
true

{:ok, true} = FunWithFlags.enable(:pipeweed, for_percentage_of: {:actors, 0.685})

FunWithFlags.enabled?(:pipeweed, for: sam)
true

FunWithFlags.Actor.Percentage.score(pippin, :pipeweed)
0.510528564453125
FunWithFlags.Actor.Percentage.score(pippin, :mushrooms)
0.6050872802734375
FunWithFlags.Actor.Percentage.score(pippin, :palantir)
0.144073486328125

Once a %-of-actors gate has been defined for a flag, the same actor will always see the same result (unless its actor or group gates are set, or the flag gets globally enabled). Also, this means that as long the percentage value of the gate will increase and never decrease, actors for which the gate has been enabled will always see it enabled.

This is ideal to gradually roll out new functionality to users.

For example, in a Phoenix application:

FunWithFlags.clear(:new_design)
FunWithFlags.enable(:new_design, for_percentage_of: {:actors, 0.2})
FunWithFlags.enable(:new_design, for_group: "beta_testers")


defmodule MyPhoenixApp.MyView do
  use MyPhoenixApp, :view

  def render("my_template.html", assigns) do
    if FunWithFlags.enabled?(:new_design, for: assigns.user) do
      render("new_template.html", assigns)
    else
      render("old_template.html", assigns)
    end
  end
end

The %-of-actors gate is incompatible and mutually exclusive with the %-of-time gate, and it replaces it when it gets set. While there are ways to make them work together, it would needlessly overcomplicate the priority rules.

Clearing a Feature Flag's Rules

Sometimes enabling or disabling a gate is not what you want, and removing that gate's rules would be more correct. For example, if you don't need anymore to explicitly enable or disable a flag for an actor or for a group, and the default state should be used instead, clearing the gate is the right choice.

More examples:

alias FunWithFlags.TestUser, as: User
harry = %User{id: 1, name: "Harry Potter", groups: ["wizards", "gryffindor"]}
hagrid = %User{id: 2, name: "Rubeus Hagrid", groups: ["wizards", "gamekeeper"]}
dudley = %User{id: 3, name: "Dudley Dursley", groups: ["muggles"]}
FunWithFlags.disable(:wands)
FunWithFlags.enable(:wands, for_group: "wizards")
FunWithFlags.disable(:wands, for_actor: hagrid)

FunWithFlags.enabled?(:wands)
false
FunWithFlags.enabled?(:wands, for: harry)
true
FunWithFlags.enabled?(:wands, for: hagrid)
false
FunWithFlags.enabled?(:wands, for: dudley)
false

FunWithFlags.clear(:wands, for_actor: hagrid)

FunWithFlags.enabled?(:wands, for: hagrid)
true

FunWithFlags.clear(:wands, for_group: "wizards")

FunWithFlags.enabled?(:wands, for: hagrid)
false
FunWithFlags.enabled?(:wands, for: harry)
false

FunWithFlags.enable(:magic_powers, for_percentage_of: {:time, 0.0001})
FunWithFlags.clear(:magic_powers, for_percentage: true)

For completeness, clearing the boolean gate is also supported.

FunWithFlags.enable(:wands)

FunWithFlags.enabled?(:wands)
true
FunWithFlags.enabled?(:wands, for: harry)
true
FunWithFlags.enabled?(:wands, for: hagrid)
false
FunWithFlags.enabled?(:wands, for: dudley)
true

FunWithFlags.clear(:wands, boolean: true)

FunWithFlags.enabled?(:wands)
false
FunWithFlags.enabled?(:wands, for: harry)
true
FunWithFlags.enabled?(:wands, for: hagrid)
false
FunWithFlags.enabled?(:wands, for: dudley)
false

It's also possible to clear an entire flag.

FunWithFlags.clear(:wands)

FunWithFlags.enabled?(:wands)
false
FunWithFlags.enabled?(:wands, for: harry)
false
FunWithFlags.enabled?(:wands, for: hagrid)
false
FunWithFlags.enabled?(:wands, for: dudley)
false

Web Dashboard

An optional extension of this library is FunWithFlags.UI, a web graphical control panel. It's a Plug, so it can be embedded in a host Phoenix or Plug application or served standalone.

Origin

This library is heavily inspired by the flipper Ruby gem.

Having used Flipper in production at scale, this project aims to improve in two main areas:

  • Minimize the load on the persistence layer: feature flags are not toggled that often, and there is no need to query Redis or the DB for each check.
  • Be more reliable: it should keep working with the latest cached values even if Redis becomes unavailable, although with the risk of nodes getting out of sync. (if the DB becomes unavailable, feature flags are probably the last of your problems)

Just as Elixir and Phoenix are meant to scale better than Ruby on Rails with high levels of traffic and concurrency, FunWithFlags should aim to be more scalable and reliable than Flipper.

So, caching, huh?

There are only two hard things in Computer Science: cache invalidation and naming things.

-- Phil Karlton

The reason to add an ETS cache is that, most of the time, feature flags can be considered static values. Doing a round-trip to the DB (Redis, PostgreSQL or MySQL) is expensive in terms of time and in terms of resources, especially if multiple flags must be checked during a single web request. In the worst cases, the load on the DB can become a cause of concern, a performance bottleneck or the source of a system failure.

Often the solution is to memoize the flag values in the context of the web request, but the approach can be extended to the scope of the entire server. This is what FunWithFlags does, as each application node/instance caches the flags in an ETS table.

Of course, caching adds a different kind of complexity and there are some pros and cons. When a flag is created or updated the ETS cache on the local node is updated immediately, and the main problem is synchronizing the flag data across the other application nodes that should share the same view of the world.

For example, if we have two or more nodes running the application, and on one of them an admin user updates a flag that the others have already cached, or creates a flag that the others have already looked up (and cached as "disabled"), then the other nodes must be notified of the changes.

FunWithFlags uses three mechanisms to deal with the problem:

  1. Use PubSub to emit change notifications. All nodes subscribe to the same channel and reload flags in the ETS cache when required.
  2. If that fails, the cache has a configurable TTL. Reading from the DB every few minutes is still better than doing so 30k times per second.
  3. If that doesn't work, it's possible to disable the cache and just read from the DB all the time. That's what Flipper does.

In terms of performance, very synthetic benchmarks (where the DBs run on the same machine as the Beam code, so with no network hop but sharing the CPU) show that the ETS cache makes querying the FunWithFlags interface between 10 and 20 times faster than going directly to Redis, and between 20 and 40 times faster than going directly to Postgres. The variance depends on the complexity of the flag data to be retrieved.

To Do

  • Add some optional randomness to the TTL, so that Redis or the DB don't get hammered at constant intervals after a server restart.

Installation

The package can be installed by adding fun_with_flags to your list of dependencies in mix.exs.

In order to have a small installation footprint, the dependencies for the different adapters are all optional. You must explicitly require the ones you wish to use.

def deps do
  [
    {:fun_with_flags, "~> 1.12.0"},

    # either:
    {:redix, "~> 0.9"},
    # or:
    {:ecto_sql, "~> 3.0"},

    # optionally, if you don't want to use Redis' builtin pubsub
    {:phoenix_pubsub, "~> 2.0"},
  ]
end

Using ecto_sql for persisting the flags also requires an ecto adapter, e.g. postgrex, mariaex or myxql. Please refer to the Ecto documentation for the details.

Since FunWithFlags depends on an Elixir more recent than 1.4, there is no need to explicitly declare the application.

If you need to customize how the :fun_with_flags application is loaded and started, refer to the Application Start Behaviour section, below in this document.

Configuration

The library can be configured in host applications through Mix and the config.exs file. This example shows some default values:

config :fun_with_flags, :cache,
  enabled: true,
  ttl: 900 # in seconds

# the Redis persistence adapter is the default, no need to set this.
config :fun_with_flags, :persistence,
  [adapter: FunWithFlags.Store.Persistent.Redis]

# this can be disabled if you are running on a single node and don't need to
# sync different ETS caches. It won't have any effect if the cache is disabled.
# The Redis PuSub adapter is the default, no need to set this.
config :fun_with_flags, :cache_bust_notifications,
  [enabled: true, adapter: FunWithFlags.Notifications.Redis]

# Notifications can also be disabled, which will also remove the Redis/Redix dependency
config :fun_with_flags, :cache_bust_notifications, [enabled: false]

When using Redis for persistence and/or cache-busting PubSub it is necessary to configure the connection to the Redis instance. These options can be omitted if Redis is not being used. For example, the defaults:

# the Redis options will be forwarded to Redix.
config :fun_with_flags, :redis,
  host: "localhost",
  port: 6379,
  database: 0

# a URL string can be used instead
config :fun_with_flags, :redis, "redis://localhost:6379/0"

# or a {URL, [opts]} tuple
config :fun_with_flags, :redis, {"redis://localhost:6379/0", socket_opts: [:inet6]}

# a {:system, name} tuple can be used to read from the environment
config :fun_with_flags, :redis, {:system, "REDIS_URL"}

Redis Sentinel is also supported. See the Redix docs for more details.

config :fun_with_flags, :redis,
  sentinel: [
    sentinels: ["redis:://locahost:1234/1"],
    group: "primary",
  ],
  database: 5

Persistence Adapters

The library comes with two persistence adapters for the Redix and Ecto libraries, that allow to persist feature flag data in Redis, PostgreSQL, MySQL, or SQLite. In order to use any of them, you must declare the correct optional dependency in the Mixfile (see the installation instructions, above).

The Redis adapter is the default and there is no need to explicitly declare it. All it needs is the Redis connection configuration.

In order to use the Ecto adapter, an Ecto repo must be provided in the configuration. FunWithFlags expects the Ecto repo to be initialized by the host application, which also needs to start and supervise any required processes. If using Phoenix this is managed automatically by the framework, and it's fine to use the same repo used by the rest of the application.

Only PostgreSQL (via postgrex), MySQL (via mariaex or myxql), and SQLite (via ecto_sqlite3) are supported at the moment. Support for other RDBMSs might come in the future.

To configure the Ecto adapter:

# Normal Phoenix and Ecto configuration.
# The repo can either use the Postgres or MySQL adapter.
config :my_app, ecto_repos: [MyApp.Repo]
config :my_app, MyApp.Repo,
  username: "my_db_user",
  password: "my secret db password",
  database: "my_app_dev",
  hostname: "localhost",
  pool_size: 10

# FunWithFlags configuration.
config :fun_with_flags, :persistence,
  adapter: FunWithFlags.Store.Persistent.Ecto,
  repo: MyApp.Repo,
  ecto_table_name: "your_table_name", # optional, defaults to "fun_with_flags_toggles"
  ecto_primary_key_type: :binary_id # optional, defaults to :id
  # For the primary key type, see also: https://hexdocs.pm/ecto/3.10.3/Ecto.Schema.html#module-schema-attributes

It's also necessary to create the DB table that will hold the feature flag data. To do that, create a new migration in your project and copy the contents of the provided migration file. Then run the migration.

When using the Ecto persistence adapter, FunWithFlags will annotate all queries using the Ecto Repo Query API with a custom option: [fun_with_flags: true]. This is done to make it easier to identify FunWithFlags queries when working with Ecto customization hooks, e.g. the Ecto.Repo.prepare_query/3 callback. Since this sort of annotations via custom query options are only useful with the Ecto Query API (context), other repo functions are not annotated with the custom option.

Ecto Multi-tenancy

If you followed the Ecto guide on setting up multi-tenancy with foreign keys, you must add an exception for queries originating from FunWithFlags. As mentioned in the section above, these queries have a custom query option named :fun_with_flags set to true:

# Sample code, only relevant if you followed the Ecto guide on multi tenancy with foreign keys.
defmodule MyApp.Repo do
  use Ecto.Repo, otp_app: :my_app

  require Ecto.Query

  @impl true
  def prepare_query(_operation, query, opts) do
    cond do
      # add the check for opts[:fun_with_flags] here:
      opts[:skip_org_id] || opts[:schema_migration] || opts[:fun_with_flags] ->
        {query, opts}

      org_id = opts[:org_id] ->
        {Ecto.Query.where(query, org_id: ^org_id), opts}

      true ->
        raise "expected org_id or skip_org_id to be set"
    end
  end
end

Ecto Custom Primary Key Types

The library defaults to using an integer (bigserial) as the type of the id primary key column. If, for any reason, you need the ID to be a UUID, you can configure it to be of type :binary_id. To do that, you need to:

  1. Set the :ecto_primary_key_type configuration option to :binary_id.
  2. Use :binary_id as the type of the :id column in the provided migration file.

PubSub Adapters

The library comes with two PubSub adapters for the Redix and Phoenix.PubSub libraries. In order to use any of them, you must declare the correct optional dependency in the Mixfile. (see the installation instructions, below)

The Redis PubSub adapter is the default and doesn't need to be explicitly configured. It can only be used in conjunction with the Redis persistence adapter however, and is not available when using Ecto for persistence. When used, it connects directly to the Redis instance used for persisting the flag data.

The Phoenix PubSub adapter uses the high level API of Phoenix.PubSub, which means that under the hood it could use either its PG2 or Redis adapters, and this library doesn't need to know. It's provided as a convenient way to leverage distributed Erlang when using FunWithFlags in a Phoenix application, although it can be used independently (without the rest of the Phoenix framework) to add PubSub to Elixir apps running on Erlang clusters.
FunWithFlags expects the Phoenix.PubSub process to be started by the host application, and in order to use this adapter the client (name or PID) must be provided in the configuration.

For example, in Phoenix (>= 1.5.0) it would be:

# normal Phoenix configuration
config :my_app, MyApp.Web.Endpoint,
  pubsub_server: MyApp.PubSub

# FunWithFlags configuration
config :fun_with_flags, :cache_bust_notifications,
  enabled: true,
  adapter: FunWithFlags.Notifications.PhoenixPubSub,
  client: MyApp.PubSub

Or, without Phoenix:

# possibly in the application's supervision tree
children = [
  {Phoenix.PubSub, [name: :my_pubsub_process_name, adapter: Phoenix.PubSub.PG2]}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
{:ok, _pid} = Supervisor.start_link(children, opts)

# config/config.exs
config :fun_with_flags, :cache_bust_notifications,
  enabled: true,
  adapter: FunWithFlags.Notifications.PhoenixPubSub,
  client: :my_pubsub_process_name

Extensibility

Custom Persistence Adapters

This library aims to be extensible and allows users to provide their own persistence layer.

This is supported through FunWithFlags.Store.Persistent, a generic persistence behaviour that is adopted by the builtin Redis and Ecto adapters.

Custom persistence adapters can adopt the behaviour and then be configured as the persistence module in the Mix config of the user applications.

For example, an application can define this module:

defmodule MyApp.MyAlternativeFlagStore do
  @behaviour FunWithFlags.Store.Persistent
  # implement all the behaviour's callback
end

And then configure the library to use it:

config :fun_with_flags, :persistence, adapter: MyApp.MyAlternativeFlagStore

Application Start Behaviour

As explained in the Installation section, above in this document, the :fun_with_flags application will start automatically when you add the package as a dependency in your Mixfile. The :fun_with_flags application starts its own supervision tree which manages all required processes and is provided by the FunWithFlags.Supervisor module.

Sometimes, this can cause issues and race conditions if FunWithFlags is configured to rely on Erlang processes that are owned by another application. For example, if you have configured the Phoenix.PubSub cache-busting notification adapter, one of FunWithFlag's processes will immediately try to subscribe to its notifications channel using the provided PubSub process identifier. If that process is not available, FunWithFlags will retry a few times and then give up and raise an exception. This will become a problem if you're using FunWithFlags in a large application (e.g. a Phoenix app) and the :fun_with_flags application starts much faster than the Phoenix supervision tree.

In these cases, it's better to directly control how FunWithFlags starts its processes.

The first step is to add the FunWithFlags.Supervisor module directly to the supervision tree of the host application. For example, in a Phoenix app it would look like this:

defmodule MyPhoenixApp.Application do
  @moduledoc false
  use Application

  def start(_type, _args) do
    children = [
      MyPhoenixApp.Repo,
      MyPhoenixAppWeb.Telemetry,
      {Phoenix.PubSub, name: MyPhoenixApp.PubSub},
      MyPhoenixAppWeb.Endpoint,
+     FunWithFlags.Supervisor,
    ]

    opts = [strategy: :one_for_one, name: MyPhoenixApp.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # ...

Then it's necessary to configure the Mix project to not start the :fun_with_flags application automatically. This can be accomplished in the Mixfile in a number of ways, for example: (Note: These are alternative solutions, you don't need to do both. You must decide which is more appropriate for your setup.)

  • Option A: Declare the :fun_with_flags dependency with either the runtime: false or app: false options. (docs)
- {:fun_with_flags, "~> 1.6"},
+ {:fun_with_flags, "~> 1.6", runtime: false},

If you use releases then you'll also need to modify the releases section in mix.exs so that it loads the fun_with_flags application explicitly (since runtime: false / app: false will exclude it from the assembled release).

def project do
  [
    app: :my_phoenix_app,
+   releases: [
+     my_phoenix_app: [
+       applications: [
+         fun_with_flags: :load
+       ]
+    ]
  ]
end
  • Option B: Declare that the :fun_with_flags application is managed directly by your host application (docs).
  def application do
    [
      mod: {MyPhoenixApp.Application, []},
+     included_applications: [:fun_with_flags],
      extra_applications: [:logger, :runtime_tools]
    ]
  end

The result of those changes is that the :fun_with_flags application won't be loaded and started automatically, and therefore the FunWithFlags supervision tree won't risk to be started before the other processes in the host Phoenix application. Rather, the supervision tree will start alongside the other core Phoenix processes.

One final note on this topic is that if you're also using FunWithFlags.UI (refer to the Web Dashboard section, above in this document), then that will need to be configured as well. The reason is that :fun_with_flags is a dependency of :fun_with_flags_ui, so including the latter as a dependency will cause the former to be auto-started despite the configuration described above. To avoid this, the same configuration should be used for the :fun_with_flags_ui dependency, regardless of the approach used (Option A: runtime: false, app: false; or Option B: included_applications).

Testing

This library depends on Redis, PostgreSQL and MySQL, and you'll need them installed and running on your system in order to run the complete test suite. The tests will use the Redis db number 5 and then clean after themselves, but it's safer to start Redis in a directory where there is no dump.rdb file you care about to avoid issues. The Ecto tests will use the SQL sandbox and all transactions will be automatically rolled back.

To setup the test DB for the Ecto persistence tests, run:

MIX_ENV=test PERSISTENCE=ecto mix do ecto.create, ecto.migrate              # for postgres
rm -rf _build/test/lib/fun_with_flags/
MIX_ENV=test PERSISTENCE=ecto RDBMS=mysql mix do ecto.create, ecto.migrate  # for mysql
rm -rf _build/test/lib/fun_with_flags/
MIX_ENV=test PERSISTENCE=ecto RDBMS=sqlite mix do ecto.create, ecto.migrate  # for sqlite

Then, to run all the tests:

$ mix test.all

The test.all task will run the test suite multiple times with different configurations to exercise a matrix of options and adapters.

The Mixfile defines a few other helper tasks that allow to run the test suite with some more specific configurations.

Development

Like for testing, developing FunWithFlags requires local installations of Redis, PostgreSQL and MySQL. For work that doesn't touch the persistence adapters too closely, it's possibly simpler to just run FunWithFlags with Redis and then let CI run the tests with the other adapters.

A common workflow is to run the tests and interact with the package API in iex.

With the default configuration, iex -S mix will compile and load FunWithFlags with Redis persistence and Redis PubSub. To compile and run the package in iex with Ecto and Phoenix PubSub support instead, use these commands:

bin/console_ecto postgres
bin/console_ecto mysql

This package uses the credo and dialyxir (dialyzer) packages to help with local development. Their mix tasks can be executed in the root directory of the project:

mix credo
mix dialyzer

Working with PubSub Locally

It's possible to test the PubSub functionality locally, in iex.

When using Redis, it's enough to start two iex -S mix sessions in two terminals, and they'll talk with one another via Redis.

When using Phoenix.PubSub (which is typically the case with Ecto), then the process is similar but you must establish a connection between the two Erlang nodes running in the two terminals. There are a number of ways to do this, and the simplest is to do it manually within iex.

Steps:

  1. Run bin/console_pubsub foo in one terminal.
  2. Run bin/console_pubsub bar in another terminal.
  3. In either terminal, grab the current name with Node.self(). (The name will also be shown in the iex prompts).
  4. In the other terminal, run Node.connect(:"THE_OTHER_NODE_NAME"). Keep in mind that the names are atoms.
  5. In either terminal, run Node.list() to check that there is a connection.

Done that, modifying any flag data in either terminal will notify the other one via PubSub.

fun_with_flags's People

Contributors

asummers avatar bobbymcwho avatar cadamsraven avatar chubarovnick avatar connorlay avatar coryodaniel avatar duldr avatar eamontaaffe avatar iamvery avatar iurimadeira avatar jc00ke avatar kelvinst avatar kianmeng avatar lostkobrakai avatar lukeledet avatar mariusbutuc avatar mbuffa avatar paulostazeski avatar rodrigues avatar seangeo avatar skylerparr avatar steffende avatar stewart avatar tompave avatar tylerbarker avatar up2jj avatar whatyouhide avatar zaid avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fun_with_flags's Issues

New release with PR #54

Anything planned for the next release? I need the content of #54
Do you have any feature in mind to include? Maybe I can help you 😄

Configure set of permitted flags

First, congrats on the lib! I really like the features and it's being super useful :)

One thing that I was feeling the need, is to somehow configure a set of the available flags, something like this:

config :fun_with_flags, 
  permitted_flags: [
    :feature_a,
    :feature_b,
    :feature_c,
  ]

I can see 2 advantages of having this:

  1. When we are calling functions like FunWithFlags.enabled?(:feature_a) or FunWithFlags.enable(:feature_a), we validate against the permitted flags. Useful to ensure there's not typo in the flag name, for example.
  2. Listing the available flags in the UI even if they don't exist in the storage. This is because I always need to check the code before add them in through the UI, not a big deal, bit if they are already listed it would be a very nice thing :)

The 1st advantage could required a relatively big refactoring and for me is not that important. It's just a plus, I think.

The 2nd advantage I really like to have it, since it makes easier to work only with the UI. This could be implemented in 2 ways, I think:

  1. Implementing in this repository, doing the logic inside all_flag_names/0 and all_flags/0
  2. Implementing in the https://github.com/tompave/fun_with_flags_ui repository.

I guess I could "override" the storage I'm using, creating my own adapter and doing these logics for the all_flag_names/0 and all_flags/0, but having this in the library can be easier for users :)

Do you think this is a valid feature to having in this library?

Fix small typo

Great lib! Found a small typo, thought I'd remove it. #37. Hope this helps!

[Question] Ensuring feature flags are in sync across dev environments

Hi,

Do you have any suggestions for ensuring that everyone's development / test environments are in sync when folks add new feature flags? We'd love some form of a version controlled config file that at the very least indicates the presence of flags, and even better, the desired flag state / gate / actor, so that each developer doesn't have to know what flags to set in their DB.

Cheers,
Oliver

(UndefinedFunctionError) function Ecto.Query.from/2 is undefined or private

Hey guys, Im trying to use Elixir 1.13 with this package, but Im getting the following error:

==> fun_with_flags
Compiling 21 files (.ex)

== Compilation error in file lib/fun_with_flags/store/persistent/ecto.ex ==
** (UndefinedFunctionError) function Ecto.Query.from/2 is undefined or private. However there is a macro with the same name and arity. Be sure to require Ecto.Query if you intend to invoke this macro
    (ecto 3.7.1) expanding macro: Ecto.Query.from/2
    lib/fun_with_flags/store/persistent/ecto.ex:29: FunWithFlags.Store.Persistent.Ecto.get/1
could not compile dependency :fun_with_flags, "mix compile" failed. Errors may have been logged above. You can recompile this dependency with "mix deps.compile fun_with_flags", update it with "mix deps.update fun_with_flags" or clean it with "mix deps.clean fun_with_flags"

These are the versions Im running:

Erlang/OTP 24 [erts-12.1.5] [source] [64-bit] [smp:6:6] [ds:6:6:10] [async-threads:1] [jit]
Elixir 1.13.0 (compiled with Erlang/OTP 24)
fun_with_flags: 1.8.0
Ecto: 3.7.1
phoenix: 1.6.4

Any clue?

Thanks!

Non atom group names

Is there a reason why group names must be atoms?

We are trying to release a feature to some users via a "bucket" method. Something like "users_bucket:1", where all the users where rem(id, 10) == 1 would have access to the feature. But with only atom groups, implementing this is kinda cumbersome. Also, kinda unsafe, as a lot of dynamic atoms would be created, and we all know how the BEAM feels about having atoms dynamically generated 😅

Exploring the codebase, I couldn't find anything that explains the validation. Did I miss any points? If not, can I submit a PR removing the need for atom group names? I can come up with something to maintain backwards compatibility ^^

[RFC] Allow disabled gates for % of Actor

The docs specify the following:

The percentage gates can only express an enabled state, as disabling something for a percentage of time or actors is logically equivalent to enabling it for the complementary percentage.

However, because actor gates override group gates, there is a hole in certain contexts.

For instance, if we want to allow all members of a group to have access to a new feature, but limit that group to a subset of actors, this is currently not possible as the actor gate overrides. For this to work, one would have to enable the group, but disable a % of the actors.

An alternative strategy, albeit one with significant change, would be to evaluate all flags, and only pass if all resolve true. The same override functionality as currently exists could be achieved by providing a veto flag on a given gate.

Perhaps we're simply missing something about the setup though, and this type of subset limiting already exists.

config is not applied

Hi,

I'm setting this up in a Phoenix application. Like this:

config :fun_with_flags, :cache,
  enabled: true,
  ttl: 900

config :fun_with_flags, :persistence,
  adapter: FunWithFlags.Store.Persistent.Ecto,
  repo: Plus.Repo,
  ecto_table_name: "fun_with_flags_toggles"

config :fun_with_flags, :cache_bust_notifications,
  enabled: true,
  adapter: FunWithFlags.Notifications.PhoenixPubSub,
  client: Plus.PubSub

Migration is setup, ran and I checked that the table is there.
However when I use the API, the error looks like the repo is not configured:

iex ▶ FunWithFlags.enabled?(:my_feat)

▶▶▶
** (RuntimeError) The NullEctoRepo doesn't implement this. You must configure a proper repo to persist flags with Ecto.
    (fun_with_flags 1.5.1) lib/fun_with_flags/store/persistent/ecto/null_repo.ex:9: FunWithFlags.NullEctoRepo.all/1
    (fun_with_flags 1.5.1) lib/fun_with_flags/store/persistent/ecto.ex:32: FunWithFlags.Store.Persistent.Ecto.get/1
    (fun_with_flags 1.5.1) lib/fun_with_flags/store.ex:15: FunWithFlags.Store.lookup/1
    (fun_with_flags 1.5.1) lib/fun_with_flags.ex:77: FunWithFlags.enabled?/2

Checking the config in the same iex session:

iex|2 ▶ Application.get_env(:fun_with_flags, :persistence)
[
  adapter: FunWithFlags.Store.Persistent.Ecto,
  repo: Plus.Repo,
  ecto_table_name: "fun_with_flags_toggles"
]

What am I missing?

[Suggestion] Gauging interest for allowing more datatypes within a flag

We have a case where we'd like the ability to toggle a numeric range as a flag. While this isn't purely a feature flag, it has overlap with FWF in the sense that we need a UI to edit values at runtime. A hypothetical API could look like this:

FunWithFlags.enable(:high_quality_threshold, values: %{
  enabled_value: 10..25,
  disabled_value: 0..0
})

FunWithFlags.enabled?(:high_quality_threshold) # true
FunWithFlags.value(:high_quality_threshold) # 10..25

FunWithFlags.disable(:high_quality_threshold)
FunWithFlags.enabled?(:high_quality_threshold) # false
FunWithFlags.value(:high_quality_threshold) # 0..0

This hasn't been fully thought through with regards to how this would interact with actors, gates etc. Furthermore, any type aside from booleans, strings, ints, and nil may be out of scope for an initial attempt.


We're likely going to be building this functionality on top of FWF for our own purposes, but I'd like to know if there would be interest in a PR for this feature with the intent of getting it in the base library/UI library. If so, I'd also like to hear any thoughts you may have on API design.

Usage with PostgresSQL schemas

I have a Phoenix Umbrella project with a bunch of apps using PostgreSQL. Each app is configured with a @schema_prefix schema attribute so that they can all access the same database, but using different schemas. That way I can keep each app's data logically separated from the others, without the need to create multiple databases in the same PostgreSQL instance, for example.

However, when I try do use FunWithFlags within one of these apps, the lib tries to find the table on the default public schema, and of course, it can't find it and it errors out. Is there a way to configure this in FunWithFlags? I've tried a few things like including the schema name in the option ecto_table_name, but with no success.

Persistence config not recognized in release

Versions

erlang 23.1
elixir 1.11.3-otp-23
fun_with_flags 1.6.0

Problem

When running our application in a production release FWF doesn't seem to be accessing our configs/persistence adapter and is raising an exception when trying to call FunWithFlags.all_flags().

Config:

config :fun_with_flags, :persistence,
  adapter: FunWithFlags.Store.Persistent.Ecto,
  repo: MyApp.Repo

Exception raised when visiting the /feature-flags/flags endpoints of the UI:

function FunWithFlags.Store.Persistent.Redis.all_flags/0 is undefined (module FunWithFlags.Store.Persistent.Redis is not available)

The application runs without issues in the local dev environment. I see in 1.5.1 there were some related changes and that there is an open issue related to running the app in a production release but I haven't been able to identify the cause, any insight would be greatly appreciated.

Thanks!

Feature Request: Postgres/Ecto support

Currently Molasses is the only feature flag library in Elixir available that has Postgres backing as an option, though I consider the abstraction of groups much cleaner in this library. Would it be possible to add a persistence layer for Postgres?

Looking through the code, I was looking for where that would get hooked in. as some sort of generic "Store Protocol/Behaviour", to see if that would even be possible with the current implementation, but the current stores (simple_store.ex and store.ex) don't seem to reference one. FunWithFlags.Store.Persistent seems to be hard coded to start a Redis worker, so perhaps that being made into a pluggable interface of sorts would more easily allow this, potentially opening the door to Mongo adapters or RethinkDB adapters or even Application config backed adapters.

GenServer FunWithFlags.Notifications.PhoenixPubSub terminating

This issue is cropping up randomly when trying to run our Phoenix server. Sometimes it fails hard, sometimes it works fine. It's unclear if this is an actual issue with FunWithFlags, or if it's a knock-on issue from something else, so please bear with me. Also, the issue appears to involve Logger to some extent.

elixir: "~> 1.6"

Logger config:

config :logger, :console,
  format: "$time $metadata[$level] $message\n",
  metadata: [:request_id]

FunWithFlags config:

config :fun_with_flags, :cache,
  enabled: true,
  ttl: 300

config :fun_with_flags, :persistence,
  adapter: FunWithFlags.Store.Persistent.Ecto,
  repo: Planswell.Api.Repo

config :fun_with_flags, :cache_bust_notifications,
  enabled: true,
  adapter: FunWithFlags.Notifications.PhoenixPubSub,
  client: lanswell.Api.PubSub

Stack trace:

11:42:17.890 [error] GenServer FunWithFlags.Notifications.PhoenixPubSub terminating
** (RuntimeError) Tried to subscribe to Phoenix.PubSub process Planswell.Api.PubSub 5 times. Giving up.
    (fun_with_flags) lib/fun_with_flags/notifications/phoenix_pubsub.ex:83: FunWithFlags.Notifications.PhoenixPubSub.subscribe/1
    (fun_with_flags) lib/fun_with_flags/notifications/phoenix_pubsub.ex:117: FunWithFlags.Notifications.PhoenixPubSub.handle_info/2
    (stdlib) gen_server.erl:616: :gen_server.try_dispatch/4
    (stdlib) gen_server.erl:686: :gen_server.handle_msg/6
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: {:subscribe_retry, 6}
==> common

=ERROR REPORT==== 26-Mar-2018::11:42:18 ===
** Generic server 'Elixir.FunWithFlags.Notifications.PhoenixPubSub' terminating 
** Last message in was {subscribe_retry,2}
** When Server state == <<"FUUOChZ9VDiTI3sxNTIyLCA3ODkzNywgNjgyNzA5fQ">>
** Reason for termination == 
** {#{'__exception__' => true,'__struct__' => 'Elixir.RuntimeError',
      message =>
          <<"cannot use Logger, the :logger application is not running">>},
    [{'Elixir.Logger.Config','__data__',0,
         [{file,"lib/logger/config.ex"},{line,53}]},
     {'Elixir.Logger',bare_log,3,[{file,"lib/logger.ex"},{line,613}]},
     {'Elixir.FunWithFlags.Notifications.PhoenixPubSub',handle_info,2,
         [{file,"lib/fun_with_flags/notifications/phoenix_pubsub.ex"},
          {line,116}]},
     {gen_server,try_dispatch,4,[{file,"gen_server.erl"},{line,616}]},
     {gen_server,handle_msg,6,[{file,"gen_server.erl"},{line,686}]},
     {proc_lib,init_p_do_apply,3,[{file,"proc_lib.erl"},{line,247}]}]}

=INFO REPORT==== 26-Mar-2018::11:42:18 ===
    application: fun_with_flags
    exited: shutdown
    type: temporary
** (Mix) Could not start application api: {:not_running, :fun_with_flags}

Setting cache `enabled: false` crashes the app

If we disable the cache in config via setting the enabled flag to false then the app crashes when trying to save a new feature flag:

exited in: GenServer.call(FunWithFlags.Store.Cache, {:put, %FunWithFlags.Flag{gates: [%FunWithFlags.Gate{enabled: false, for: nil, type: :boolean}], name: :asd}}, 5000) ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started

Is this setting meant to be used in some other way?

v1.4.0 release date?

Hello @tompave

Is v1.4.0 going to be released soon(ish)?
I really like the changes you introduced with the behavior for Persistance that allows to create custom adapters, curious if it's going to be available in the upcoming days to try it out.

FunWithFlags.Supervisor not loaded when using mix releases

In order to make the use of the new FunWithFlags.Supervisor, I added runtime: false to both fun_with_flags and fun_with_flags_ui, but it failed to run in production with an error that FunWithFlags.Supervisor is not found.

The following seems to fix the problem:

      releases: [
        myapp: [
          applications: [fun_with_flags: :load, fun_with_flags_ui: :load]
        ]
      ]

Is there something I'm missing or should the above be added to the docs for the people who are using mix release to package their app?

In memory store

I was previously using the fun_with_flags_in_memory library for getting/setting feature flags in our test environment. The repo now seem to have been deleted, and the package isn't compatible with 1.5 as it depends on 1.4.0. Would it make sense to bring this store into the main library, explicitly for people unit testing? If not, is there another preferred option for this use case?

Cannot compile without redix optional dependency.

On Elixir 1.9.1, Erlang 21, when I try to get the project working with only the

{:fun_with_flags, "~> 1.4.1"},

line to mix.exs and then add the following configuration to a brand new phoenix project:

config :fun_with_flags, :cache_bust_notifications,
  enabled: true,
  adapter: FunWithFlags.Notifications.PhoenixPubSub,
  client: Flaggy.PubSub

config :fun_with_flags, :persistence,
  adapter: FunWithFlags.Store.Persistent.Ecto,
  repo: Flaggy.Repo

(Flaggy is the name of the phoenix app)
(I'm trying to avoid redis entirely)

I get the following compile error:

code/elixir/flaggy mix phx.server
Compiling 13 files (.ex)
Generated flaggy app
[error] FunWithFlags: It looks like you're trying to use FunWithFlags.Store.Persistent.Redis to persist flags, but you haven't added its optional dependency to the Mixfile of your project.
[info] Application fun_with_flags exited: exited in: FunWithFlags.Application.start(:normal, [])
    ** (EXIT) an exception was raised:
        ** (UndefinedFunctionError) function FunWithFlags.Store.Persistent.Redis.worker_spec/0 is undefined (module FunWithFlags.Store.Persistent.Redis is not available)
            FunWithFlags.Store.Persistent.Redis.worker_spec()
            (fun_with_flags) lib/fun_with_flags/application.ex:28: FunWithFlags.Application.persistence_spec/0
            (fun_with_flags) lib/fun_with_flags/application.ex:17: FunWithFlags.Application.children/0
            (fun_with_flags) lib/fun_with_flags/application.ex:10: FunWithFlags.Application.start/2
            (kernel) application_master.erl:277: :application_master.start_it_old/4
** (Mix) Could not start application fun_with_flags: exited in: FunWithFlags.Application.start(:normal, [])
    ** (EXIT) an exception was raised:
        ** (UndefinedFunctionError) function FunWithFlags.Store.Persistent.Redis.worker_spec/0 is undefined (module FunWithFlags.Store.Persistent.Redis is not available)
            FunWithFlags.Store.Persistent.Redis.worker_spec()
            (fun_with_flags) lib/fun_with_flags/application.ex:28: FunWithFlags.Application.persistence_spec/0
            (fun_with_flags) lib/fun_with_flags/application.ex:17: FunWithFlags.Application.children/0
            (fun_with_flags) lib/fun_with_flags/application.ex:10: FunWithFlags.Application.start/2
            (kernel) application_master.erl:277: :application_master.start_it_old/4

When I add redix to mix.exs everything works normally. It's not terrible to add the redix library, but I would like to not, as I'm not actually using it for anything.

Feature Proposal: Audit Logging

It would be great to have the ability to track who toggled what features and to what. This can be useful both from a troubleshooting standpoint but also to satisfy auditing/compliance requirements.

It would also be lovely if this worked with https://github.com/tompave/fun_with_flags_ui, the more seamless the better. As such this proposal encompasses both libraries, although the changes to FunWithFlags do stand on their own.

FunWithFlags changes.

I'd like to add a new function:
FunWithFlags.add_audit_log(action, flag_name, current_user, opts)

It would be called like so:

FunWithFlags.add_audit_log(:enable, :my_feature, 1234, [for_percentage_of: {:time, ratio}])

Under the hood this would call a new callback on the Persistent behavior:

c:FunWithFlags.Store.Persistent.add_audit_log/4 which we'd implement for both
Redix and Ecto.

These function implementations would simply persist an audit log to either a new redis key or a new postgres table. This would require updating the readme to include this new table in a migration (shouldn't be difficult to make this optional).

Additionally, it could be useful to also add another function to the Persistent behavior which wraps other things in a transaction. This would be useful in fun_with_flags_ui portion of these changes to ensure that we don't have situations where an audit log gets added but the feature flag doesn't change and vice versa. Thinking a function signature would look something like:

c:FunWithFlags.Store.Persistent.transaction/1 which would just call Ecto.Transaction or Redix.transaction_pipeline/3 essentially.

All of the above changes I believe can stand alone as useful features to FunWithFlags.

FunWithFlags.UI changes.

There are a number of places in FunWithFlagsUI that call FunWithFlags.enable/2,FunWithFlags.disable/2, and FunWithFlags.clear/2 directly.

The idea is to wrap those calls in new functions defined in FunWithFlags.UI.Utils that call out to the previously mentioned FunWithFlags.Store.Persistent.transaction/1 and then calls FunWithFlags.enable/2,FunWithFlags.disable/2, or FunWithFlags.clear/2, as well as FunWithFlags.add_audit_log/4.

The one missing piece at this point is the method of retrieving the current user.

Would love feedback on this next part:

We could make some assumptions or have some opinions.

We could enforce that the current user is always in conn.assigns.fun_with_flags_current_user and leave it up to the users of FunWithFlags.UI to add a plug which puts the current user there.

Or we could allow users to define a function that accepts the %Plug.Conn{} struct as an argument and spits out the current user. This is way more flexible, and doesn't force users to add something to assigns, but it's slightly more elaborate to configure. You'd have to pass a reference to this function into application config.

We could also go even further and define a behavior with a callback that accepts a %Plug.Conn{} struct and outputs the current user, and then force users to implement that callback on a module that they can then pass in as config. This is also a bit elaborate.

Regardless of which one we go with, we'd want to configure this behavior in application config and ensure that if no :audit_logging config is supplied (should be the state of all things that have implemented this to date), then things simply work and no audit logging is attempted. This should be an opt-in feature.

Audit Logging UI

The above is all we need to get audit logging implemented and working fairly seemlessly, but we still wouldn't have a way to actually view the persisted audit logs without looking in Redis or Postgres itself. So we'd probably want to add a UI in FunWithFlags.UI for viewing the audit logs related to a given key. This could happen separately from all of the other changes.

In Conclusion

What do you think of this proposal? I think I understand enough about this project at this point to make these changes, but super open to feedback!

If you think this sounds like a reasonable new feature, I'd be happy to implement it. The only breaking changes I could think of is the additional callbacks on the FunWithFlags.Store.Persistent behavior, but at the very least it should warn people that have written custom persistence modules at compile time that they need to implement a new thing.

I'm confident that this can be built in a way that doesn't disrupt existing users (beyond those that have custom persistence) and allows a user to not configure audit logging and simply ignore this feature.

Web UI

Any plans on adding a simple Web UI to this project?

I would suggest creating the web UI as part of this project for ease of setup.

Support Phoenix PubSub 2.0

Phoenix 1.5.0-rc0 is now out with Phoenix PubSub 2.0, however fun_with_flags currently depends on phoenix_pubsub "~> 1.0".

null value in column "id" violates not-null constraint

I was getting an error message when enabling a flag from postgrex:

** (Postgrex.Error) ERROR 23502 (not_null_violation): null value in column "id" violates not-null constraint

In my application I have Ecto configured to use UUIDs by default. You could add some logic to the Record field to figure this out probably, although, it may be simpler to modify the migration for the table to explicitly set the primary key to an serial type:

create table(:fun_with_flags_toggles, primary_key: false) do
  add :id, :serial, primary_key: true
  add :flag_name, :string
  add :gate_type, :string
  add :target, :string
  add :enabled, :boolean
end

Cluster-wide flags

This project uses pubsub now, but the issue of what to do if a node joins, how to tell if the local cache is good, etc, is one of those notoriously sticky wickets.

If it used Phoenix.Tracker, it could have stronger guarantees about whether a feature 'should be' on or off cluster-wide, without worrying as much about keeping a local ets in sync itself -- at a cost of potentially increased latency in rolling out flag changes (just because of time required to be sure of consensus compared to straight pubsub w/o consensus, not because Tracker's not performant at running tons of feature flag lookups, which it would be). But tuning the tracker ttls downwards works really well, and a set of feature flags, even per-user ones, is the sort of thing that Tracker should be good at given its biggest use case.

That stronger consistency could also be helpful for other features, like occasionally persisting the feature flags, because you'd know you're persisting something consistent, not a local cache that might be out of sync.

[Question] How to toggle flag in production?

Production servers in my scenario cannot run iex, so DevOps can't toggle the feature flags using the functions described in the documentation.

In essence, my question is, will toggling the flag in Redis store have the desired effect with compiled code?

[error] FunWithFlags: It looks like you're trying to use FunWithFlags.Store.Persistent.Redis to persist flags, but you haven't added its optional dependency to the Mixfile of your project.

Hey there!

Im trying to use the lib but it seems that I can't make it work with the basic configuration.

[error] FunWithFlags: It looks like you're trying to use FunWithFlags.Store.Persistent.Redis to persist flags, but you haven't added its optional dependency to the Mixfile of your project.

In my config.exs I have:

# FunWithFlags configuration.
config :fun_with_flags, :persistence,
  adapter: FunWithFlags.Store.Persistent.Ecto,
  repo: EventsApi.Repo

I do have in my mix file

    {:ecto_sql, "~> 3.0}", 
    {:phoenix_pubsub, "~> 1.1"},

Elixir version:

Erlang/OTP 22 [erts-10.4.4] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [hipe] [dtrace]

Elixir 1.9.2 (compiled with Erlang/OTP 20)

I haven't found anything else in the docs that need to be changed. What am I doing wrong?

Thanks

'conflict_target' in Ecto persistence prevents use with MySQL

Howdy! I'm attempting to integrate fun_with_flags into a Phoenix app that's using a MySQL database. Unfortunately, at the moment it doesn't quite behave as expected:

iex(1)> FunWithFlags.enable(:test_flag)
** (ArgumentError) The :conflict_target option is not supported in insert/insert_all by MySQL
    (ecto_sql) lib/ecto/adapters/mysql/connection.ex:972: Ecto.Adapters.MySQL.Connection.error!/2
    (ecto_sql) lib/ecto/adapters/mysql/connection.ex:149: Ecto.Adapters.MySQL.Connection.insert/6
    (ecto_sql) lib/ecto/adapters/mysql.ex:208: Ecto.Adapters.MySQL.insert/6
    (ecto) lib/ecto/repo/schema.ex:651: Ecto.Repo.Schema.apply/4
    (ecto) lib/ecto/repo/schema.ex:264: anonymous fn/15 in Ecto.Repo.Schema.do_insert/3
    (fun_with_flags) lib/fun_with_flags/store/persistent/ecto.ex:193: FunWithFlags.Store.Persistent.Ecto.do_insert/3
    (fun_with_flags) lib/fun_with_flags/store/persistent/ecto.ex:71: FunWithFlags.Store.Persistent.Ecto.put/2
    (fun_with_flags) lib/fun_with_flags/store.ex:36: FunWithFlags.Store.put/2
    (fun_with_flags) lib/fun_with_flags.ex:185: FunWithFlags.enable/2

It should be possible to add this option to the insert statement dynamically, based on the result of Repo.__adapter__ (AFAIK, MySQL is the special case here) - but I'm not certain if that's the best way to approach this.

If you'd welcome a patch implementing the above suggestion, let me know.

Improved runtime config support

Hi - I want to use this library in our app, however because we run it in multiple different environments from a single release, all our config comes in through environment variables (we also do this to supply passwords without putting them in hardwired config).

While it's possible to provide a Redis URL using the {:system, url} magic, it's kind of clunky trying to munge the individual bits of config (SSL, password, server etc) into another environment variable before startup, and seems like pointless effort when internally Redix is just going to split them all up again. Plus it still doesn't allow any other bits (TTL etc) to be configured dynamically.

There's generally two ways to fix this that I've seen - my preferred one is to add generalised support using Confex. That requires no changes to the config spec, but allows each individual field to be specified as {:system, :type, "ENV_VAR", default}.

However some people don't like having an added dependency in their library, so an alternative is to allow something in the config like {:callback, Module, :function} and have the application provide a function which returns config.

I'm happy to implement whichever of those you prefer (or some alternative) - just let me know.

Compiler Warning that Repo is unavailable

Hi and thanks for your work on this library.

I get compiler warnings when I first compile the code:

==> fun_with_flags
Compiling 20 files (.ex)
warning: function Demo.Repo.all/1 is undefined (module Demo.Repo is not available)
Found at 3 locations:
  lib/fun_with_flags/store/persistent/ecto.ex:23
  lib/fun_with_flags/store/persistent/ecto.ex:157
  lib/fun_with_flags/store/persistent/ecto.ex:166

warning: function Demo.Repo.delete_all/1 is undefined (module Demo.Repo is not available)
Found at 3 locations:
  lib/fun_with_flags/store/persistent/ecto.ex:92
  lib/fun_with_flags/store/persistent/ecto.ex:119
  lib/fun_with_flags/store/persistent/ecto.ex:144

warning: function Demo.Repo.insert/2 is undefined (module Demo.Repo is not available)
  lib/fun_with_flags/store/persistent/ecto.ex:193

warning: function Demo.Repo.one/1 is undefined (module Demo.Repo is not available)
  lib/fun_with_flags/store/persistent/ecto.ex:44

warning: function Demo.Repo.transaction/1 is undefined (module Demo.Repo is not available)
  lib/fun_with_flags/store/persistent/ecto.ex:42

warning: function Demo.Repo.update/2 is undefined (module Demo.Repo is not available)
  lib/fun_with_flags/store/persistent/ecto.ex:200

because it looks like this module attribute gets compiled before my app is actually ready (since it gets compiled after fun_with_flags):

Outside of the warning, it doesn't appear to cause any issues but I thought I would bring it up in case it was unknown.

Changing `ecto_table_name` config requires recompile

The ecto_table_name config is evaluated at compile time here: https://github.com/tompave/fun_with_flags/blob/master/lib/fun_with_flags/store/persistent/ecto/record.ex#L11

However this uses Application.get_env in Config.ecto_table_name by way of Config.persistence_config.

The result is that changing the :fun_with_flags, :persistence, :ecto_table_name config has no effect unless FunWithFlags is recompiled.

I would expect FunWithFlags to reflect the config changes, as it does with the rest of the persistence settings.

I found some info here that shows how to set a table name at runtime for an existing schema (with Ecto.put_meta/2).

If this sounds right, I could put together a PR.

Can't change config without doing a `mix deps.clean fun_with_flags`.

Since fun_with_flags sets the config at compile time, when the config is changed, you have to do a mix deps.clean fun_with_flags. If you don't, when you compile the app, you get a

ERROR! the application :fun_with_flags has a different value set for key :cache during runtime compared to compile time. Since this application environment entry was marked as compile time, this difference can lead to different behaviour than expected:

  * Compile time value was not set
  * Runtime value was set to: [enabled: true]

To fix this error, you might:

  * Make the runtime value match the compile time one

  * Recompile your project. If the misconfigured application is a dependency, you may need to run "mix deps.compile fun_with_flags --force"

  * Alternatively, you can disable this check. If you are using releases, you can set :validate_compile_env to false in your release configuration. If you are using Mix to start your system, you can pass the --no-validate-compile-env flag


==> fun_with_flags
could not compile dependency :fun_with_flags, "mix compile" failed. Errors may have been logged above. You can recompile this dependency with "mix deps.compile fun_with_flags", update it with "mix deps.update fun_with_flags" or clean it with "mix deps.clean fun_with_flags"

This causes problems with our CI run, and requires us to issue a mix deps.clean fun_with_flags as one of the commands so our builds won't get stuck.

"Looks like ... Redis" when using ecto

This isn't a bug, but I did fumble through getting fun_with_flags set up for a while. I turns out I wasn't planning on using caching because I was working in dev/test environment trying to set up fun_with_flags with Ecto. I kept getting an error message despite setting Ecto as my adapter saying that I was missing a Redis dependency.

It turns out this error was specifically because I had neglected to turn caching off and it defaults to Redis.

Do you think this is better addressed in the README or modifying the error message to reflect that you can also simply disable caching.

Is there any reason flag names must be atoms instead of strings?

The only benefits of atom names I can come up with are slightly faster comparison and less memory usage, but I don't think they would make a considerable difference for a real app. And I didn't even take into account that flag names are already converted to strings (and vice versa) by persistence adapters.

The issues with atom names arise when considering flag names that come from external input (suppose I want to provide this library to non-elixir services by wrapping it in JSON API). Because we cannot just convert arbitrary strings to atoms, it would require using String.to_existing_atom/1 and rescuing from exceptions in case the flag name does not exist (as you already do in the web UI).

Schema prefix support for Ecto

Would it be possible to add schema support for ecto via prefixes so funwithflags can live in a separate schema in postgres. Useful in a multi-tenancy situation.

I can work around it by customizing the persistence adapter, but it is a fairly simple fix, and happy to submit a patch (I don't know about how it would work in MySQL). Will require changes in two files: ecto.ex and config.ex and also introduce a new name called ecto_table_prefix

lobo

Allow different persistence adapters per env

Currently, if you specify :redis config options in your main config, there is no way to overwrite this config in a different environment. For example, when writing a test config and overriding the adapter defined by :fun_with_flags, :persistence, the adapter gets overwritten but it still attempts to make a connection to a Redis instance as there is still the :fun_with_flags, :redis config that is defined on the main config file but not in say a test.exs config.

ie
config.exs

  config :fun_with_flags, :redis,
    host: <host>
    port: <port>

test.exs

config :fun_with_flags, :cache_bust_notifications, enabled: false

config :fun_with_flags, :persistence, adapter: MyApp.CustomFlagStore

Merged config would be the following:

#Still attempts to connect to redis as this is defined even though a non redis adapter is defined below
  config :fun_with_flags, :redis, 
    host: <host>
    port: <port>

config :fun_with_flags, :cache_bust_notifications, enabled: false

config :fun_with_flags, :persistence, adapter: MyApp.CustomFlagStore

Specifying a customer persistence adapter should not attempt to make a connection to ecto or redis

Non-boolean variations

G'day! Having already had to use at least one tri-state variation on our previous solution, I'm nervous about solving the same kind of problem by strapping together FWF's boolean flags. All off is fine, but >1 on would be confusing.

Don't suppose we could support any atom, not just :true and :false? It'd need an Ecto migration to migrate that boolean enabled to a string state… what else?

Request: Consider improving the documentation to be a little more straightforward

Hi!

I guess I have a good use case to use this lib in my application but I found the documentation just overwhelmingly wordy. Also, I could not quite comprehend what configuration I need after reading the whole README file - I still don't quite know how to configure this for my use-case. For example: Do I need to configure cache_bust_notifications, PubSub adapters, and whatnot to just use this with Phoenix and Ecto?.

One thing that I've missed is a Getting Started section that goes about installing and configuring the library. Right now, both the configuration and installation sections are almost at the end of the README file and a lot of things seem to be explained in parallel to each other.

My suggestion is to extract the installation, configuration, and usage sections into a Get Started, struct and explain them in a little bit more focused fashion and let the miscellaneous part at the end (Web Dashboard, Origin, Caching, PubSub, etc). If possible, provide a complete installation/ configuration example for the simplest use-case, using the Ecto adapter in a Phoenix app, and then talk about using Redis and extensibility.

¹BTW: Why don't you use Ecto or something like Mnesia as the default configuration for persistence? I think that there's a lot of Elixir libs that over-rely on external dependencies to do this without actually needing it. I'm not quite sure why would Redisbe necessary for something like this.

Config param `cache: enabled` results in exception

Hi!
We've been using your library for some time - it's wonderful, thanks for all the effort you put into it.

We've started noticing a strange pattern when using FunWithFlags for unit tests. Our setup consists of a postgres table that is used as the backend for the library, where all the flags are saved. We opted for the db backend in tests since Phoenix offers isolation for accessing the database during tests, while there is no such thing for redit.

However, the fact that your library implements a caching mechanism via ETS seems to provide shared behaviour between tests, which we want to avoid.

Thus we set the following params inside our test config:

config :fun_with_flags, :cache, enabled: false

And we keep getting crashes because the library tries to call the cache genserver anyway... The offending function seems to be this one, which gets called after each operation of reading/saving flags, whatever the value of the :cache property is.

I think it would be pretty easy to check the config before calling Cache.put or Cache.get, in order to avoid crashes... What do you think?

Notifications need to start with the application supervisor

The Notification module has to wait on the Application that contains the PubSub to start otherwise it fails, now this is handled with retries but they break really easily and littler tests.

I know that the notifications are now optional but I think the right approach is just to have the Application supervisor that spawns the PubSub also spawn the the Notification module.

Here are the errors:

FunWithFlags: Cannot subscribe to Phoenix.PubSub process MyApp.PubSub (exception: %ArgumentError{message: "argument error"})
FunWithFlags: retrying to subscribe to Phoenix.PubSub, attempt 2

Compilation failure with elixir-1.13.0-rc.0

fun_with_flags 1.6.0 fails to compile under elixir-1.13.0-rc.0, with the following error:

$ mix deps.compile fun_with_flags
==> fun_with_flags
Compiling 7 files (.ex)

== Compilation error in file lib/fun_with_flags/protocols/actor.ex ==
** (CompileError) lib/fun_with_flags/protocols/actor.ex:116: undefined function defdelegate/2 (there is no such import)

Per the Elixir 1.13.0 release notes:

[Protocol] Add defdelegate to the list of unallowed macros inside protocols as protocols do not allow function definitions

Exception when using Ecto for persistence without cache

I set up FunWithFlags with the following config. (I'm running a low volume site on a single node, and was happy to take the performance hit to not have to set up Redis.)

config :fun_with_flags, :cache, enabled: false
config :fun_with_flags, :cache_bust_notifications, enabled: false

config :fun_with_flags, :persistence,
  adapter: FunWithFlags.Store.Persistent.Ecto,
  repo: Poplar.Repo

It worked absolutely fine in dev, but when I deployed to production (compiled into a release) I got the following exception on boot:

13:26:29.420 [info] Application fun_with_flags exited: exited in: FunWithFlags.Application.start(:normal, [])
** (EXIT) an exception was raised:
** (UndefinedFunctionError) function FunWithFlags.Store.Persistent.Redis.worker_spec/0 is undefined (module FunWithFlags.Store.Persistent.Redis is not available)
FunWithFlags.Store.Persistent.Redis.worker_spec()
(fun_with_flags) lib/fun_with_flags/application.ex:16: FunWithFlags.Application.children/0
(fun_with_flags) lib/fun_with_flags/application.ex:10: FunWithFlags.Application.start/2
(kernel) application_master.erl:277: :application_master.start_it_old/4

Is this a problem with my configuration, or have I hit a bug in the library? Thanks!

Spec definition and documentation mismatch

Thank you for creating this library. I use it often. I recently added rules for groups and the dialyzer caught that I wasn't using atoms for groups. On further investigation it looks as though FunWithFlagsUI inserts the group rule into the database as a String and is passed to the in? function as a String (https://github.com/tompave/fun_with_flags/blob/master/lib/fun_with_flags/protocols/group.ex#L68). But, the specs specify that the group should be an atom. As far as I can tell it comes from the database unchanged:

iex(99)> FunWithFlags.get_flag(:bar)
2019-12-04 14:11:39.911 [info] [application=ecto_sql module=Ecto.Adapters.SQL function=log/4 pid=<0.1619.0> ] QUERY OK source="fun_with_flags_toggles" db=0.7ms
SELECT DISTINCT f0.`flag_name` FROM `fun_with_flags_toggles` AS f0 []
%FunWithFlags.Flag{
  gates: [
    %FunWithFlags.Gate{enabled: false, for: nil, type: :boolean},
    %FunWithFlags.Gate{
      enabled: true,
      for: "inserted_at<2019-11-8",
      type: :group
    }
  ],
  name: :bar
}

The documentation also shows the usage as strings (https://github.com/tompave/fun_with_flags#group-gate). My guess is that the spec should be atom() | String.t() or term().

Store provider is set on compile time

Hello 👋

when working with FunWithFlags in our test suite we noticed that we have to recompile the fun_with_flags dependency every time we changed the config around the storage provider (we wanted to simply disable the cache in the test environment).

 ** (exit) exited in: GenServer.call(FunWithFlags.Store.Cache, {:put, %FunWithFlags.Flag{gates: [%FunWithFlags.Gate{enabled: true, for: "user:...", type: :actor}], name: :foo}}, 5000)
 ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't starte

This happens, because the config is stored in a module variable:
https://github.com/tompave/fun_with_flags/blob/master/lib/fun_with_flags.ex#L30

So when we changed the config to disable caching, it would still use the StoreProvider instead of the SimpleStoreProvider, but with the cache process not started.

Would it make sense to move this call from a module variable @store into a function store(), so that the config is still changeable without needing to recompile the project?

I can happily help out and open a PR if you agree :)

Also thank you for creating this great project btw 💚. It really is a joy to use and helps us a great deal in our workflows.

Cheers
Andi

Error in multi tenant apps with foreign key

Hey,

first of all, thanks for release this great library for us all. I think I've found an issue when using Fun With Flags with a multi tenant app. I've implemented my multi tenant model following this ecto guide: https://hexdocs.pm/ecto/multi-tenancy-with-foreign-keys.html.

The problem is when I try to enable/disable a flag, I got the error:

** (RuntimeError) expected org_id or skip_org_id to be set

Obviously, the error occurs because FWF doesn't know anything about my tenant model. I propose to add a fun_with_flags: true option for every call of Ecto.Repo inside the lib. By doing that, I can figure out which queries are originated from Fun With Flags and execute these queries outside my tenant model, like this:

defmodule MyApp.Repo do
  use Ecto.Repo, otp_app: :my_app

  require Ecto.Query

  def prepare_query(operation, query, opts) do
    cond do
      opts[:skip_org_id] || opts[:schema_migration] || opts[:fun_with_flags] ->
        {query, opts}

      org_id = opts[:org_id] ->
        {Ecto.Query.where(query, org_id: ^org_id), opts}

      true ->
        raise "expected org_id or skip_org_id to be set"
    end
  end
end

I can work on that and send you a pull request. Do you think there's a chance for you to merge it?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.