{"id":13507909,"url":"https://github.com/tompave/fun_with_flags","last_synced_at":"2025-05-13T17:14:11.967Z","repository":{"id":16709954,"uuid":"80388272","full_name":"tompave/fun_with_flags","owner":"tompave","description":"Feature Flags/Toggles for Elixir","archived":false,"fork":false,"pushed_at":"2025-04-05T08:25:21.000Z","size":864,"stargazers_count":1119,"open_issues_count":6,"forks_count":84,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-04-25T14:50:31.108Z","etag":null,"topics":["ecto","elixir","feature-flags","feature-toggles","phoenix-framework","redis"],"latest_commit_sha":null,"homepage":"https://hexdocs.pm/fun_with_flags/FunWithFlags.html","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tompave.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2017-01-30T02:28:03.000Z","updated_at":"2025-04-24T19:41:59.000Z","dependencies_parsed_at":"2023-11-26T22:30:46.447Z","dependency_job_id":"e954cbde-5dda-413a-b3e8-1c06146eebdc","html_url":"https://github.com/tompave/fun_with_flags","commit_stats":{"total_commits":706,"total_committers":29,"mean_commits":"24.344827586206897","dds":0.07223796033994334,"last_synced_commit":"16c569fd164341abde185e929bfaf9c02e1db24d"},"previous_names":[],"tags_count":42,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tompave%2Ffun_with_flags","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tompave%2Ffun_with_flags/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tompave%2Ffun_with_flags/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tompave%2Ffun_with_flags/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tompave","download_url":"https://codeload.github.com/tompave/fun_with_flags/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253990500,"owners_count":21995776,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ecto","elixir","feature-flags","feature-toggles","phoenix-framework","redis"],"created_at":"2024-08-01T02:00:42.629Z","updated_at":"2025-05-13T17:14:06.956Z","avatar_url":"https://github.com/tompave.png","language":"Elixir","funding_links":[],"categories":["Feature Flags and Toggles","Elixir","Fully Open-Source Projects"],"sub_categories":["Technology-specific Implementations"],"readme":"# FunWithFlags\n\n[![Mix Tests](https://github.com/tompave/fun_with_flags/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/tompave/fun_with_flags/actions/workflows/test.yml?query=branch%3Amaster)\n[![Code Quality](https://github.com/tompave/fun_with_flags/actions/workflows/quality.yml/badge.svg?branch=master)](https://github.com/tompave/fun_with_flags/actions/workflows/quality.yml?query=branch%3Amaster)  \n[![Hex.pm](https://img.shields.io/hexpm/v/fun_with_flags.svg)](https://hex.pm/packages/fun_with_flags)\n[![hexdocs.pm](https://img.shields.io/badge/docs-1.13.0-brightgreen.svg)](https://hexdocs.pm/fun_with_flags/1.13.0/FunWithFlags.html)\n[![Hex.pm Downloads](https://img.shields.io/hexpm/dt/fun_with_flags)](https://hex.pm/packages/fun_with_flags)\n[![License](https://img.shields.io/hexpm/l/fun_with_flags.svg)](https://github.com/tompave/fun_with_flags/blob/master/LICENSE.txt)\n[![ElixirWeekly](https://img.shields.io/badge/featured-ElixirWeekly-8e5ab5.svg)](https://elixirweekly.net/issues/43)\n[![ElixirCasts](https://img.shields.io/badge/featured-ElixirCasts-ff931e.svg)](https://elixircasts.io/feature-flags)\n\nFunWithFlags, the Elixir feature flag library.\n\nIf you're reading this on the [GitHub repo](https://github.com/tompave/fun_with_flags), 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](https://hexdocs.pm/fun_with_flags/readme.html).\n\n---\n\nFunWithFlags 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](#web-dashboard) as control panel.\n\nIt 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.\n\n## Content\n\n* [What's a Feature Flag](#whats-a-feature-flag)\n* [Usage](#usage)\n  - [Gate Priority and Interactions](#gate-priority-and-interactions)\n  - [Boolean Gate](#boolean-gate)\n  - [Actor Gate](#actor-gate)\n  - [Group Gate](#group-gate)\n  - [Percentage of Time Gate](#percentage-of-time-gate)\n  - [Percentage of Actors Gate](#percentage-of-actors-gate)\n  - [Clearing a Feature Flag's Rules](#clearing-a-feature-flags-rules)\n* [Web Dashboard](#web-dashboard)\n* [Origin](#origin)\n* [So, caching, huh?](#so-caching-huh)\n* [To Do](#to-do)\n* [Installation](#installation)\n* [Configuration](#configuration)\n  - [Persistence Adapters](#persistence-adapters)\n    - [Ecto Multi-tenancy](#ecto-multi-tenancy)\n    - [Ecto Custom Primary Key Types](#ecto-custom-primary-key-types)\n  - [PubSub Adapters](#pubsub-adapters)\n* [Extensibility](#extensibility)\n  - [Custom Persistence Adapters](#custom-persistence-adapters)\n* [Telemetry](#telemetry)\n* [Application Start Behaviour](#application-start-behaviour)\n* [Testing](#testing)\n* [Development](#development)\n  - [Working with PubSub Locally](#working-with-pubsub-locally)\n  - [Benchmarks](#benchmarks)\n\n## What's a Feature Flag?\n\nFeature 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.\n\nIn 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.\n\nThe goal is to have more granular and precise control over what is made available to which users, and when.\nA 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.\nThey can also be used to implement a simple authorization system, for example to an admin area.\n\n\n## Usage\n\nFunWithFlags 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.\n\nDifferent kinds of toggle gates are supported:\n\n* **Boolean**: globally on and off.\n* **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.\n* **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.\n* **%-of-Time**: globally on for a percentage of the time. It ignores actors and groups. Mutually exclusive with the %-of-actors gate.\n* **%-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.\n\nBoolean, 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.\n\n### Gate Priority and Interactions\n\nThe priority order is from most to least specific: `Actors \u003e Groups \u003e Boolean \u003e Percentage`, and it applies to both enabled and disabled gates.\n\nFor 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.\n\nAs 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.\n\n### Boolean Gate\n\nThe 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.\n\n```elixir\nFunWithFlags.enabled?(:cool_new_feature)\nfalse\n\n{:ok, true} = FunWithFlags.enable(:cool_new_feature)\n\nFunWithFlags.enabled?(:cool_new_feature)\ntrue\n\n{:ok, false} = FunWithFlags.disable(:cool_new_feature)\n\nFunWithFlags.enabled?(:cool_new_feature)\nfalse\n```\n\n### Actor Gate\n\nThis 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.\n\nActor gates take precedence over the others, both when they're enabled and when they're disabled. They can be considered as toggle overrides.\n\nIn 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.\n\n\n```elixir\ndefmodule MyApp.User do\n  defstruct [:id, :name]\nend\n\ndefimpl FunWithFlags.Actor, for: MyApp.User do\n  def id(%{id: id}) do\n    \"user:#{id}\"\n  end\nend\n\nbruce = %MyApp.User{id: 1, name: \"Bruce\"}\nalfred = %MyApp.User{id: 2, name: \"Alfred\"}\n\nFunWithFlags.Actor.id(bruce)\n\"user:1\"\nFunWithFlags.Actor.id(alfred)\n\"user:2\"\n\ndefimpl FunWithFlags.Actor, for: Map do\n  def id(%{actor_id: actor_id}) do\n    \"map:#{actor_id}\"\n  end\n\n  def id(map) do\n    map\n    |\u003e inspect()\n    |\u003e (\u0026:crypto.hash(:md5, \u00261)).()\n    |\u003e Base.encode16\n    |\u003e (\u0026\"map:#{\u00261}\").()\n  end\nend\n\nFunWithFlags.Actor.id(%{actor_id: \"bar\"})\n\"map:bar\"\nFunWithFlags.Actor.id(%{foo: \"bar\"})\n\"map:E0BB5BA6873E3AC34B0B6928190C1F2B\"\n```\n\nWith the protocol implemented, actors can be used with the library functions:\n\n```elixir\n{:ok, true} = FunWithFlags.enable(:restful_nights)\n{:ok, false} = FunWithFlags.disable(:restful_nights, for_actor: bruce)\n{:ok, true} = FunWithFlags.enable(:batmobile, for_actor: bruce)\n\nFunWithFlags.enabled?(:restful_nights)\ntrue\nFunWithFlags.enabled?(:batmobile)\nfalse\n\nFunWithFlags.enabled?(:restful_nights, for: alfred)\ntrue\nFunWithFlags.enabled?(:batmobile, for: alfred)\nfalse\n\nFunWithFlags.enabled?(:restful_nights, for: bruce)\nfalse\nFunWithFlags.enabled?(:batmobile, for: bruce)\ntrue\n```\n\nActor 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:\n\n```elixir\ndefimpl FunWithFlags.Actor, for: MyApp.User do\n  def id(user) do\n    \"user:#{user.id}\"\n  end\nend\n\ndefimpl FunWithFlags.Actor, for: MyApp.Country do\n  def id(country) do\n    \"country:#{country.iso3166}\"\n  end\nend\n```\n\n### Group Gate\n\nGroup 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.\n\nGroup gates take precedence over boolean gates but are overridden by actor gates.\n\nGroup names can be binaries or atoms. Atoms are supported for retro-compatibility with versions `\u003c= 0.9` and binaries are therefore preferred. In fact, atoms are internally converted to binaries and are then stored and later retrieved as binaries.\n\nThe semantics to determine which entities belong to which groups are application specific.\nEntities 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}`.\n\nIn 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.\n\nThe protocol can be implemented for custom structs or literally any other type.\n\n\n```elixir\ndefmodule MyApp.User do\n  defstruct [:email, admin: false, groups: []]\nend\n\ndefimpl FunWithFlags.Group, for: MyApp.User do\n  def in?(%{email: email}, \"employee\"), do: Regex.match?(~r/@mycompany.com$/, email)\n  def in?(%{admin: is_admin}, \"admin\"), do: !!is_admin\n  def in?(%{groups: list}, group_name), do: group_name in list\nend\n\nelisabeth = %MyApp.User{email: \"elisabeth@mycompany.com\", admin: true, groups: [\"engineering\", \"product\"]}\nFunWithFlags.Group.in?(elisabeth, \"employee\")\ntrue\nFunWithFlags.Group.in?(elisabeth, \"admin\")\ntrue\nFunWithFlags.Group.in?(elisabeth, \"engineering\")\ntrue\nFunWithFlags.Group.in?(elisabeth, \"marketing\")\nfalse\n\ndefimpl FunWithFlags.Group, for: Map do\n  def in?(%{group: group_name}, group_name), do: true\n  def in?(_, _), do: false\nend\n\nFunWithFlags.Group.in?(%{group: \"dumb_tests\"}, \"dumb_tests\")\ntrue\n```\n\nWith the protocol implemented, actors can be used with the library functions:\n\n```elixir\nFunWithFlags.disable(:database_access)\nFunWithFlags.enable(:database_access, for_group: \"engineering\")\n\nFunWithFlags.enabled?(:database_access)\nfalse\nFunWithFlags.enabled?(:database_access, for: elisabeth)\ntrue\n```\n\n### Percentage of Time Gate\n\n%-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.\n\nWhen a %-of-time gate is checked a [pseudo-random number is generated](http://erlang.org/doc/man/rand.html#uniform-1) 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.\n\n%-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.\n\nA 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.\n\nFor example:\n\n```elixir\nFunWithFlags.clear(:alternative_implementation)\nFunWithFlags.enable(:alternative_implementation, for_percentage_of: {:time, 0.05})\n\ndef foo(bar) do\n  if FunWithFlags.enabled?(:alternative_implementation) do\n    new_foo(bar)\n  else\n    old_foo(bar)\n  end\nend\n```\n\nThe %-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.\n\n### Percentage of Actors Gate\n\n%-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:\n\n```elixir\nactor\n|\u003e FunWithFlags.Actor.id()\n|\u003e sha256_hash(flag_name)\n|\u003e hash_to_percentage()\n```\n\nSince 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.\n\nJust 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.\n\nA practical example, based on the `FunWithFlags.Actor` protocol set up from the previous sections:\n\n```elixir\ndefmodule MyApp.User do\n  defstruct [:id, :name]\nend\n\ndefimpl FunWithFlags.Actor, for: MyApp.User do\n  def id(%{id: id}) do\n    \"user:#{id}\"\n  end\nend\n\nfrodo  = %MyApp.User{id: 1, name: \"Frodo Baggins\"}\nsam    = %MyApp.User{id: 2, name: \"Samwise Gamgee\"}\npippin = %MyApp.User{id: 3, name: \"Peregrin Took\"}\nmerry  = %MyApp.User{id: 4, name: \"Meriadoc Brandybuck\"}\n\nFunWithFlags.Actor.Percentage.score(frodo, :pipeweed)\n0.8658294677734375\nFunWithFlags.Actor.Percentage.score(sam, :pipeweed)\n0.68426513671875\nFunWithFlags.Actor.Percentage.score(pippin, :pipeweed)\n0.510528564453125\nFunWithFlags.Actor.Percentage.score(merry, :pipeweed)\n0.2617645263671875\n\n{:ok, true} = FunWithFlags.enable(:pipeweed, for_percentage_of: {:actors, 0.60})\n\nFunWithFlags.enabled?(:pipeweed, for: frodo)\nfalse\nFunWithFlags.enabled?(:pipeweed, for: sam)\nfalse\nFunWithFlags.enabled?(:pipeweed, for: pippin)\ntrue\nFunWithFlags.enabled?(:pipeweed, for: merry)\ntrue\n\n{:ok, true} = FunWithFlags.enable(:pipeweed, for_percentage_of: {:actors, 0.685})\n\nFunWithFlags.enabled?(:pipeweed, for: sam)\ntrue\n\nFunWithFlags.Actor.Percentage.score(pippin, :pipeweed)\n0.510528564453125\nFunWithFlags.Actor.Percentage.score(pippin, :mushrooms)\n0.6050872802734375\nFunWithFlags.Actor.Percentage.score(pippin, :palantir)\n0.144073486328125\n```\n\n\nOnce 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.\n\nThis is ideal to gradually roll out new functionality to users.\n\nFor example, in a Phoenix application:\n\n```elixir\nFunWithFlags.clear(:new_design)\nFunWithFlags.enable(:new_design, for_percentage_of: {:actors, 0.2})\nFunWithFlags.enable(:new_design, for_group: \"beta_testers\")\n\n\ndefmodule MyPhoenixApp.MyView do\n  use MyPhoenixApp, :view\n\n  def render(\"my_template.html\", assigns) do\n    if FunWithFlags.enabled?(:new_design, for: assigns.user) do\n      render(\"new_template.html\", assigns)\n    else\n      render(\"old_template.html\", assigns)\n    end\n  end\nend\n```\n\nThe %-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.\n\n### Clearing a Feature Flag's Rules\n\nSometimes 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.\n\nMore examples:\n\n```elixir\nalias FunWithFlags.TestUser, as: User\nharry = %User{id: 1, name: \"Harry Potter\", groups: [\"wizards\", \"gryffindor\"]}\nhagrid = %User{id: 2, name: \"Rubeus Hagrid\", groups: [\"wizards\", \"gamekeeper\"]}\ndudley = %User{id: 3, name: \"Dudley Dursley\", groups: [\"muggles\"]}\nFunWithFlags.disable(:wands)\nFunWithFlags.enable(:wands, for_group: \"wizards\")\nFunWithFlags.disable(:wands, for_actor: hagrid)\n\nFunWithFlags.enabled?(:wands)\nfalse\nFunWithFlags.enabled?(:wands, for: harry)\ntrue\nFunWithFlags.enabled?(:wands, for: hagrid)\nfalse\nFunWithFlags.enabled?(:wands, for: dudley)\nfalse\n\nFunWithFlags.clear(:wands, for_actor: hagrid)\n\nFunWithFlags.enabled?(:wands, for: hagrid)\ntrue\n\nFunWithFlags.clear(:wands, for_group: \"wizards\")\n\nFunWithFlags.enabled?(:wands, for: hagrid)\nfalse\nFunWithFlags.enabled?(:wands, for: harry)\nfalse\n\nFunWithFlags.enable(:magic_powers, for_percentage_of: {:time, 0.0001})\nFunWithFlags.clear(:magic_powers, for_percentage: true)\n```\n\nFor completeness, clearing the boolean gate is also supported.\n\n```elixir\nFunWithFlags.enable(:wands)\n\nFunWithFlags.enabled?(:wands)\ntrue\nFunWithFlags.enabled?(:wands, for: harry)\ntrue\nFunWithFlags.enabled?(:wands, for: hagrid)\nfalse\nFunWithFlags.enabled?(:wands, for: dudley)\ntrue\n\nFunWithFlags.clear(:wands, boolean: true)\n\nFunWithFlags.enabled?(:wands)\nfalse\nFunWithFlags.enabled?(:wands, for: harry)\ntrue\nFunWithFlags.enabled?(:wands, for: hagrid)\nfalse\nFunWithFlags.enabled?(:wands, for: dudley)\nfalse\n```\n\nIt's also possible to clear an entire flag.\n\n```elixir\nFunWithFlags.clear(:wands)\n\nFunWithFlags.enabled?(:wands)\nfalse\nFunWithFlags.enabled?(:wands, for: harry)\nfalse\nFunWithFlags.enabled?(:wands, for: hagrid)\nfalse\nFunWithFlags.enabled?(:wands, for: dudley)\nfalse\n```\n\n## Web Dashboard\n\nAn optional extension of this library is [`FunWithFlags.UI`](https://github.com/tompave/fun_with_flags_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.\n\n\n## Origin\n\nThis library is heavily inspired by the [flipper Ruby gem](https://github.com/jnunemaker/flipper).\n\nHaving used Flipper in production at scale, this project aims to improve in two main areas:\n\n* 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.\n* 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)\n\nJust 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.\n\n## So, caching, huh?\n\n\u003e There are only two hard things in Computer Science: cache invalidation and naming things.\n\u003e\n\u003e -- Phil Karlton\n\nThe 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.\n\nOften 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.\n\nOf 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.\n\nFor 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.\n\nFunWithFlags uses three mechanisms to deal with the problem:\n\n1. Use PubSub to emit change notifications. All nodes subscribe to the same channel and reload flags in the ETS cache when required.\n2. 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.\n3. 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.\n\nIn 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.\n\n## To Do\n\n* Add some optional randomness to the TTL, so that Redis or the DB don't get hammered at constant intervals after a server restart.\n\n## Installation\n\nThe package can be installed by adding `fun_with_flags` to your list of dependencies in `mix.exs`.\n\nIn 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.\n\n```elixir\ndef deps do\n  [\n    {:fun_with_flags, \"~\u003e 1.13.0\"},\n\n    # either:\n    {:redix, \"~\u003e 0.9\"},\n    # or:\n    {:ecto_sql, \"~\u003e 3.0\"},\n\n    # optionally, if you don't want to use Redis' builtin pubsub\n    {:phoenix_pubsub, \"~\u003e 2.0\"},\n  ]\nend\n```\n\nUsing `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.\n\nSince FunWithFlags depends on an Elixir more recent than 1.4, there is [no need to explicitly declare the application](https://github.com/elixir-lang/elixir/blob/v1.4/CHANGELOG.md#application-inference).\n\nIf you need to customize how the `:fun_with_flags` application is loaded and started, refer to the [Application Start Behaviour](#application-start-behaviour) section, below in this document.\n\n## Configuration\n\nThe library can be configured in host applications through Mix and the `config.exs` file. This example shows some default values:\n\n```elixir\nconfig :fun_with_flags, :cache,\n  enabled: true,\n  ttl: 900 # in seconds\n\n# the Redis persistence adapter is the default, no need to set this.\nconfig :fun_with_flags, :persistence,\n  [adapter: FunWithFlags.Store.Persistent.Redis]\n\n# this can be disabled if you are running on a single node and don't need to\n# sync different ETS caches. It won't have any effect if the cache is disabled.\n# The Redis PuSub adapter is the default, no need to set this.\nconfig :fun_with_flags, :cache_bust_notifications,\n  [enabled: true, adapter: FunWithFlags.Notifications.Redis]\n\n# Notifications can also be disabled, which will also remove the Redis/Redix dependency\nconfig :fun_with_flags, :cache_bust_notifications, [enabled: false]\n```\n\nWhen 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:\n\n```elixir\n# the Redis options will be forwarded to Redix.\nconfig :fun_with_flags, :redis,\n  host: \"localhost\",\n  port: 6379,\n  database: 0\n\n# a URL string can be used instead\nconfig :fun_with_flags, :redis, \"redis://localhost:6379/0\"\n\n# or a {URL, [opts]} tuple\nconfig :fun_with_flags, :redis, {\"redis://localhost:6379/0\", socket_opts: [:inet6]}\n\n# a {:system, name} tuple can be used to read from the environment\nconfig :fun_with_flags, :redis, {:system, \"REDIS_URL\"}\n```\n\n[Redis Sentinel](https://redis.io/docs/manual/sentinel/) is also supported. See the [Redix docs](https://github.com/whatyouhide/redix/tree/v1.1.5#redis-sentinel) for more details.\n\n```elixir\nconfig :fun_with_flags, :redis,\n  sentinel: [\n    sentinels: [\"redis:://locahost:1234/1\"],\n    group: \"primary\",\n  ],\n  database: 5\n```\n\n### Persistence Adapters\n\nThe library comes with two persistence adapters for the [`Redix`](https://hex.pm/packages/redix) and [`Ecto`](https://hex.pm/packages/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](#installation) instructions, above).\n\nThe Redis adapter is the default and there is no need to explicitly declare it. All it needs is the Redis connection configuration.\n\nIn 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.\n\nOnly PostgreSQL (via [`postgrex`](https://hex.pm/packages/postgrex)), MySQL (via [`mariaex`](https://hex.pm/packages/mariaex) or [`myxql`](https://hex.pm/packages/myxql)), and SQLite (via [`ecto_sqlite3`](https://hex.pm/packages/ecto_sqlite3)) are supported at the moment. Support for other RDBMSs might come in the future.\n\nTo configure the Ecto adapter:\n\n```elixir\n# Normal Phoenix and Ecto configuration.\n# The repo can either use the Postgres or MySQL adapter.\nconfig :my_app, ecto_repos: [MyApp.Repo]\nconfig :my_app, MyApp.Repo,\n  username: \"my_db_user\",\n  password: \"my secret db password\",\n  database: \"my_app_dev\",\n  hostname: \"localhost\",\n  pool_size: 10\n\n# FunWithFlags configuration.\nconfig :fun_with_flags, :persistence,\n  adapter: FunWithFlags.Store.Persistent.Ecto,\n  repo: MyApp.Repo,\n  ecto_table_name: \"your_table_name\", # optional, defaults to \"fun_with_flags_toggles\"\n  ecto_primary_key_type: :binary_id # optional, defaults to :id\n  # For the primary key type, see also: https://hexdocs.pm/ecto/3.10.3/Ecto.Schema.html#module-schema-attributes\n```\n\nIt's also necessary to create the DB table that will hold the feature flag data. To do that, [create a new migration](https://hexdocs.pm/ecto_sql/Mix.Tasks.Ecto.Gen.Migration.html) in your project and copy the contents of [the provided migration file](https://github.com/tompave/fun_with_flags/blob/master/priv/ecto_repo/migrations/00000000000000_create_feature_flags_table.exs). Then [run the migration](https://hexdocs.pm/ecto_sql/Mix.Tasks.Ecto.Migrate.html).\n\nWhen using the Ecto persistence adapter, FunWithFlags will annotate all queries using the [Ecto Repo Query API](https://hexdocs.pm/ecto/3.8.4/Ecto.Repo.html#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](https://hexdocs.pm/ecto/3.8.4/Ecto.Repo.html#c:prepare_query/3). Since this sort of annotations via custom query options are only useful with the Ecto Query API ([context](https://elixirforum.com/t/proposal-prepare-query-for-write-operations/50510)), other repo functions are not annotated with the custom option.\n\n#### Ecto Multi-tenancy\n\nIf you followed the Ecto guide on setting up [multi-tenancy with foreign keys](https://hexdocs.pm/ecto/3.8.4/multi-tenancy-with-foreign-keys.html), 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`:\n\n```elixir\n# Sample code, only relevant if you followed the Ecto guide on multi tenancy with foreign keys.\ndefmodule MyApp.Repo do\n  use Ecto.Repo, otp_app: :my_app\n\n  require Ecto.Query\n\n  @impl true\n  def prepare_query(_operation, query, opts) do\n    cond do\n      # add the check for opts[:fun_with_flags] here:\n      opts[:skip_org_id] || opts[:schema_migration] || opts[:fun_with_flags] -\u003e\n        {query, opts}\n\n      org_id = opts[:org_id] -\u003e\n        {Ecto.Query.where(query, org_id: ^org_id), opts}\n\n      true -\u003e\n        raise \"expected org_id or skip_org_id to be set\"\n    end\n  end\nend\n```\n\n#### Ecto Custom Primary Key Types\n\nThe 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:\n\n  1. Set the `:ecto_primary_key_type` configuration option to `:binary_id`.\n  2. Use `:binary_id` as the type of the `:id` column in the [provided migration file](https://github.com/tompave/fun_with_flags/blob/master/priv/ecto_repo/migrations/00000000000000_create_feature_flags_table.exs).\n\n### PubSub Adapters\n\nThe library comes with two PubSub adapters for the [`Redix`](https://hex.pm/packages/redix) and [`Phoenix.PubSub`](https://hex.pm/packages/phoenix_pubsub) libraries. In order to use any of them, you must declare the correct optional dependency in the Mixfile. (see the [installation](#installation) instructions, below)\n\nThe 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.\n\nThe 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.  \nFunWithFlags 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.\n\nFor example, in Phoenix (\u003e= 1.5.0) it would be:\n\n```elixir\n# normal Phoenix configuration\nconfig :my_app, MyApp.Web.Endpoint,\n  pubsub_server: MyApp.PubSub\n\n# FunWithFlags configuration\nconfig :fun_with_flags, :cache_bust_notifications,\n  enabled: true,\n  adapter: FunWithFlags.Notifications.PhoenixPubSub,\n  client: MyApp.PubSub\n```\n\nOr, without Phoenix:\n\n```elixir\n# possibly in the application's supervision tree\nchildren = [\n  {Phoenix.PubSub, [name: :my_pubsub_process_name, adapter: Phoenix.PubSub.PG2]}\n]\nopts = [strategy: :one_for_one, name: MyApp.Supervisor]\n{:ok, _pid} = Supervisor.start_link(children, opts)\n\n# config/config.exs\nconfig :fun_with_flags, :cache_bust_notifications,\n  enabled: true,\n  adapter: FunWithFlags.Notifications.PhoenixPubSub,\n  client: :my_pubsub_process_name\n```\n\n## Extensibility\n\n### Custom Persistence Adapters\n\nThis library aims to be extensible and allows users to provide their own persistence layer.\n\nThis is supported through [`FunWithFlags.Store.Persistent`](https://github.com/tompave/fun_with_flags/blob/master/lib/fun_with_flags/store/persistent.ex), a generic persistence [behaviour](https://hexdocs.pm/elixir/typespecs.html#behaviours) that is adopted by the builtin Redis and Ecto adapters.\n\nCustom persistence adapters can adopt the behaviour and then be configured as the persistence module in the Mix config of the user applications.\n\nFor example, an application can define this module:\n\n```elixir\ndefmodule MyApp.MyAlternativeFlagStore do\n  @behaviour FunWithFlags.Store.Persistent\n  # implement all the behaviour's callback\nend\n```\n\nAnd then configure the library to use it:\n\n```elixir\nconfig :fun_with_flags, :persistence, adapter: MyApp.MyAlternativeFlagStore\n```\n## Telemetry\n\nFunWithFlags is instrumented with [Telemetry](https://hex.pm/packages/telemetry) and emits events at runtime. Please refer to the [Telemetry docs](https://hexdocs.pm/telemetry/readme.html) for detailed instructions on how to consume the emitted events.\n\nThe full list of events emitted by FunWithFlags are documented in the [FunWithFlags.Telemetry](https://hexdocs.pm/fun_with_flags/FunWithFlags.Telemetry.html) module.\n\n## Application Start Behaviour\n\nAs explained in the [Installation](#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.\n\nSometimes, 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.\n\nIn these cases, it's better to directly control how FunWithFlags starts its processes.\n\nThe 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:\n\n```diff\ndefmodule MyPhoenixApp.Application do\n  @moduledoc false\n  use Application\n\n  def start(_type, _args) do\n    children = [\n      MyPhoenixApp.Repo,\n      MyPhoenixAppWeb.Telemetry,\n      {Phoenix.PubSub, name: MyPhoenixApp.PubSub},\n      MyPhoenixAppWeb.Endpoint,\n+     FunWithFlags.Supervisor,\n    ]\n\n    opts = [strategy: :one_for_one, name: MyPhoenixApp.Supervisor]\n    Supervisor.start_link(children, opts)\n  end\n\n  # ...\n```\n\nThen 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.)\n\n* **Option A**: Declare the `:fun_with_flags` dependency with either the `runtime: false` or `app: false` options. ([docs](https://hexdocs.pm/mix/1.11.3/Mix.Tasks.Deps.html#module-dependency-definition-options))\n\n```diff\n- {:fun_with_flags, \"~\u003e 1.6\"},\n+ {:fun_with_flags, \"~\u003e 1.6\", runtime: false},\n```\n\nIf 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).\n\n```diff\ndef project do\n  [\n    app: :my_phoenix_app,\n+   releases: [\n+     my_phoenix_app: [\n+       applications: [\n+         fun_with_flags: :load\n+       ]\n+    ]\n  ]\nend\n```\n\n* **Option B**: Declare that the `:fun_with_flags` application is managed directly by your host application ([docs](https://hexdocs.pm/mix/1.11.3/Mix.Tasks.Compile.App.html)).\n\n```diff\n  def application do\n    [\n      mod: {MyPhoenixApp.Application, []},\n+     included_applications: [:fun_with_flags],\n      extra_applications: [:logger, :runtime_tools]\n    ]\n  end\n```\n\nThe 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.\n\nOne final note on this topic is that if you're also using [`FunWithFlags.UI`](https://github.com/tompave/fun_with_flags_ui) (refer to the [Web Dashboard](#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`).\n\n\n## Testing\n\nThis 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](https://github.com/tompave/fun_with_flags/blob/master/test/support/test_utils.ex#L4) 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.\n\nTo setup the test DB for the Ecto persistence tests, run:\n\n```\nMIX_ENV=test PERSISTENCE=ecto mix do ecto.create, ecto.migrate              # for postgres\nrm -rf _build/test/lib/fun_with_flags/\nMIX_ENV=test PERSISTENCE=ecto RDBMS=mysql mix do ecto.create, ecto.migrate  # for mysql\nrm -rf _build/test/lib/fun_with_flags/\nMIX_ENV=test PERSISTENCE=ecto RDBMS=sqlite mix do ecto.create, ecto.migrate  # for sqlite\n\n```\n\nThen, to run all the tests:\n```\n$ mix test.all\n```\n\nThe `test.all` task will run the test suite multiple times with different configurations to exercise a matrix of options and adapters.\n\nThe Mixfile defines a few other helper tasks that allow to run the test suite with some more specific configurations.\n\n## Development\n\nLike 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.\n\nA common workflow is to run the tests and interact with the package API in `iex`.\n\nWith the [default configuration](https://github.com/tompave/fun_with_flags/blob/master/config/config.exs), `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:\n\n```shell\nbin/console_ecto postgres\nbin/console_ecto mysql\n```\n\nThis package uses the [credo](https://hex.pm/packages/credo) and [dialyxir](https://hex.pm/packages/dialyxir) ([dialyzer](https://erlang.org/doc/man/dialyzer.html)) packages to help with local development. Their mix tasks can be executed in the root directory of the project:\n\n```shell\nmix credo\nmix dialyzer\n```\n\n### Working with PubSub Locally\n\nIt's possible to test the PubSub functionality locally, in `iex`.\n\nWhen using Redis, it's enough to start two `iex -S mix` sessions in two terminals, and they'll talk with one another via Redis.\n\nWhen 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`.\n\nSteps:\n\n1. Run `bin/console_pubsub foo` in one terminal.\n2. Run `bin/console_pubsub bar` in another terminal.\n3. In either terminal, grab the current name with `Node.self()`. (The name will also be shown in the `iex` prompts).\n4. In the other terminal, run `Node.connect(:\"THE_OTHER_NODE_NAME\")`. Keep in mind that the names are atoms.\n5. In either terminal, run `Node.list()` to check that there is a connection.\n\nDone that, modifying any flag data in either terminal will notify the other one via PubSub.\n\n### Benchmarks\n\nThe package comes with [a suite of synthetic benchmark scripts](https://github.com/tompave/fun_with_flags/tree/master/benchmarks). Their use is recommended when working on the internals of the package.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftompave%2Ffun_with_flags","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftompave%2Ffun_with_flags","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftompave%2Ffun_with_flags/lists"}