Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

All notable changes to this project will be documented in this file.

## Unreleased

### Additions/Changes

* Allow multiple actors for Flipper.enabled?. Improves performance of feature flags for multiple actors and simplifies code for users of flipper.

### Deprecations

* `:thing` in `enabled?` instrumentation payload. Use `:actors` instead.
```diff
ActiveSupport::Notifications.subscribe('enabled?.feature_operation.flipper') do |name, start, finish, id, payload|
- payload[:thing]
+ payload[:actors]
end
```

## 0.27.1

* Quick fix for missing require of "flipper/version" that was causing issues with some flipper-ui people.
Expand Down
20 changes: 20 additions & 0 deletions benchmark/enabled_multiple_actors_ips.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require 'bundler/setup'
require 'flipper'
require 'benchmark/ips'

actor1 = Flipper::Actor.new("User;1")
actor2 = Flipper::Actor.new("User;2")
actor3 = Flipper::Actor.new("User;3")
actor4 = Flipper::Actor.new("User;4")
actor5 = Flipper::Actor.new("User;5")
actor6 = Flipper::Actor.new("User;6")
actor7 = Flipper::Actor.new("User;7")
actor8 = Flipper::Actor.new("User;8")

actors = [actor1, actor2, actor3, actor4, actor5, actor6, actor7, actor8]

Benchmark.ips do |x|
x.report("with 5 actors") { Flipper.enabled?(:foo, actors) }
x.report("with 5 enabled? checks") { actors.each { |actor| Flipper.enabled?(:foo, actor) } }
x.compare!
end
4 changes: 2 additions & 2 deletions examples/active_record/internals.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
require 'flipper/adapters/active_record'

# Register a few groups.
Flipper.register(:admins) { |thing| thing.admin? }
Flipper.register(:early_access) { |thing| thing.early_access? }
Flipper.register(:admins) { |actor| actor.admin? }
Flipper.register(:early_access) { |actor| actor.early_access? }

# Create a user class that has flipper_id instance method.
User = Struct.new(:flipper_id)
Expand Down
6 changes: 3 additions & 3 deletions examples/dsl.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require 'bundler/setup'
require 'flipper'

# create a thing with an identifier
# create an actor with an identifier
class Person < Struct.new(:id)
include Flipper::Identifier
end
Expand Down Expand Up @@ -58,8 +58,8 @@ class Person < Struct.new(:id)
puts Flipper.actor(responds_to_flipper_id).inspect

# get an instance of an actor using an object
thing = Struct.new(:flipper_id).new(22)
puts Flipper.actor(thing).inspect
actor = Struct.new(:flipper_id).new(22)
puts Flipper.actor(actor).inspect

# register a top level group
admins = Flipper.register(:admins) { |actor|
Expand Down
4 changes: 2 additions & 2 deletions examples/mongo/internals.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
require 'flipper/adapters/mongo'

# Register a few groups.
Flipper.register(:admins) { |thing| thing.admin? }
Flipper.register(:early_access) { |thing| thing.early_access? }
Flipper.register(:admins) { |actor| actor.admin? }
Flipper.register(:early_access) { |actor| actor.early_access? }

# Create a user class that has flipper_id instance method.
User = Struct.new(:flipper_id)
Expand Down
4 changes: 2 additions & 2 deletions examples/redis/internals.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
client = Redis.new

# Register a few groups.
Flipper.register(:admins) { |thing| thing.admin? }
Flipper.register(:early_access) { |thing| thing.early_access? }
Flipper.register(:admins) { |actor| actor.admin? }
Flipper.register(:early_access) { |actor| actor.early_access? }

# Create a user class that has flipper_id instance method.
User = Struct.new(:flipper_id)
Expand Down
4 changes: 2 additions & 2 deletions examples/redis/namespaced.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
flipper = Flipper.new(adapter)

# Register a few groups.
Flipper.register(:admins) { |thing| thing.admin? }
Flipper.register(:early_access) { |thing| thing.early_access? }
Flipper.register(:admins) { |actor| actor.admin? }
Flipper.register(:early_access) { |actor| actor.early_access? }

# Create a user class that has flipper_id instance method.
User = Struct.new(:flipper_id)
Expand Down
4 changes: 2 additions & 2 deletions examples/sequel/internals.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
require 'flipper/adapters/sequel'

# Register a few groups.
Flipper.register(:admins) { |thing| thing.admin? }
Flipper.register(:early_access) { |thing| thing.early_access? }
Flipper.register(:admins) { |actor| actor.admin? }
Flipper.register(:early_access) { |actor| actor.early_access? }

# Create a user class that has flipper_id instance method.
User = Struct.new(:flipper_id)
Expand Down
6 changes: 3 additions & 3 deletions lib/flipper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,12 @@ def instance=(flipper)
#
# name - The Symbol name of the group.
# block - The block that should be used to determine if the group matches a
# given thing.
# given actor.
#
# Examples
#
# Flipper.register(:admins) { |thing|
# thing.respond_to?(:admin?) && thing.admin?
# Flipper.register(:admins) { |actor|
# actor.respond_to?(:admin?) && actor.admin?
# }
#
# Returns a Flipper::Group.
Expand Down
36 changes: 28 additions & 8 deletions lib/flipper/cloud/instrumenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,35 @@ def push(name, payload)
}
if (thing = payload[:thing])
dimensions["flipper_id"] = thing.value.to_s
end

event = {
type: "enabled",
dimensions: dimensions,
measures: {},
ts: Time.now.utc,
}
@brow.push event
event = {
type: "enabled",
dimensions: dimensions,
measures: {},
ts: Time.now.utc,
}
@brow.push event
elsif (actors = payload[:actors])
now = Time.now.utc
actors.each do |actor|
dimensions["flipper_id"] = actor.value.to_s
event = {
type: "enabled",
dimensions: dimensions,
measures: {},
ts: now,
}
@brow.push event
end
else
event = {
type: "enabled",
dimensions: dimensions,
measures: {},
ts: Time.now.utc,
}
@brow.push event
end
end
end
end
Expand Down
8 changes: 4 additions & 4 deletions lib/flipper/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,12 @@ def group(name)

# Public: Wraps an object as a flipper actor.
#
# thing - The object that you would like to wrap.
# actor - The object that you would like to wrap.
#
# Returns an instance of Flipper::Types::Actor.
# Raises ArgumentError if thing does not respond to `flipper_id`.
def actor(thing)
Types::Actor.new(thing)
# Raises ArgumentError if actor does not respond to `flipper_id`.
def actor(actor)
Types::Actor.new(actor)
end

# Public: Shortcut for getting a percentage of time instance.
Expand Down
6 changes: 3 additions & 3 deletions lib/flipper/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ module Flipper
# Top level error that all other errors inherit from.
class Error < StandardError; end

# Raised when gate can not be found for a thing.
# Raised when gate can not be found for an actor.
class GateNotFound < Error
def initialize(thing)
super "Could not find gate for #{thing.inspect}"
def initialize(actor)
super "Could not find gate for #{actor.inspect}"
end
end

Expand Down
22 changes: 12 additions & 10 deletions lib/flipper/feature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,19 @@ def clear
instrument(:clear) { adapter.clear(self) }
end

# Public: Check if a feature is enabled for a thing.
# Public: Check if a feature is enabled for zero or more actors.
#
# Returns true if enabled, false if not.
def enabled?(thing = nil)
thing = Types::Actor.wrap(thing) unless thing.nil?
def enabled?(*actors)
actors = actors.flatten.compact.map { |actor| Types::Actor.wrap(actor) }
actors = nil if actors.empty?

instrument(:enabled?, thing: thing) do |payload|
# thing is left for backwards compatibility
instrument(:enabled?, thing: actors&.first, actors: actors) do |payload|
context = FeatureCheckContext.new(
feature_name: @name,
values: gate_values,
thing: thing
actors: actors
)

if open_gate = gates.detect { |gate| gate.open?(context) }
Expand Down Expand Up @@ -359,14 +361,14 @@ def gate(name)
gates_hash[name.to_sym]
end

# Public: Find the gate that protects a thing.
# Public: Find the gate that protects an actor.
#
# thing - The object for which you would like to find a gate
# actor - The object for which you would like to find a gate
#
# Returns a Flipper::Gate.
# Raises Flipper::GateNotFound if no gate found for thing
def gate_for(thing)
gates.detect { |gate| gate.protects?(thing) } || raise(GateNotFound, thing)
# Raises Flipper::GateNotFound if no gate found for actor
def gate_for(actor)
gates.detect { |gate| gate.protects?(actor) } || raise(GateNotFound, actor)
end

private
Expand Down
12 changes: 8 additions & 4 deletions lib/flipper/feature_check_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ class FeatureCheckContext
# gates for the feature.
attr_reader :values

# Public: The thing we want to know if a feature is enabled for.
attr_reader :thing
# Public: The actors we want to know if a feature is enabled for.
attr_reader :actors

def initialize(feature_name:, values:, thing:)
def initialize(feature_name:, values:, actors:)
@feature_name = feature_name
@values = values
@thing = thing
@actors = actors
end

def actors?
[email protected]? && [email protected]?
end

# Public: Convenience method for groups value like Feature has.
Expand Down
23 changes: 12 additions & 11 deletions lib/flipper/gate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,29 @@ def data_type
raise 'Not implemented'
end

def enabled?(_value)
def enabled?(value)
raise 'Not implemented'
end

# Internal: Check if a gate is open for a thing. Implemented in subclass.
# Internal: Check if a gate is open for one or more actors. Implemented
# in subclass.
#
# Returns true if gate open for thing, false if not.
def open?(_thing, _value, _options = {})
# Returns true if gate open for any actor, false if not.
def open?(actors, value, options = {})
false
end

# Internal: Check if a gate is protects a thing. Implemented in subclass.
# Internal: Check if a gate is protects an actor. Implemented in subclass.
#
# Returns true if gate protects thing, false if not.
def protects?(_thing)
# Returns true if gate protects actor, false if not.
def protects?(actor)
false
end

# Internal: Allows gate to wrap thing using one of the supported flipper
# types so adapters always get something that responds to value.
def wrap(thing)
thing
# Internal: Allows gate to wrap actor using one of the supported flipper
# types so adapters always get someactor that responds to value.
def wrap(actor)
actor
end

# Public: Pretty string version for debugging.
Expand Down
19 changes: 11 additions & 8 deletions lib/flipper/gates/actor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,23 @@ def enabled?(value)
!value.empty?
end

# Internal: Checks if the gate is open for a thing.
# Internal: Checks if the gate is open for an actor.
#
# Returns true if gate open for thing, false if not.
# Returns true if gate open for actor, false if not.
def open?(context)
return false if context.thing.nil?
context.values.actors.include?(context.thing.value)
return false unless context.actors?

context.actors.any? do |actor|
context.values.actors.include?(actor.value)
end
end

def wrap(thing)
Types::Actor.wrap(thing)
def wrap(actor)
Types::Actor.wrap(actor)
end

def protects?(thing)
Types::Actor.wrappable?(thing)
def protects?(actor)
Types::Actor.wrappable?(actor)
end
end
end
Expand Down
6 changes: 4 additions & 2 deletions lib/flipper/gates/group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ def enabled?(value)
#
# Returns true if gate open for thing, false if not.
def open?(context)
return false if context.thing.nil?
return false unless context.actors?

context.values.groups.any? do |name|
Flipper.group(name).match?(context.thing, context)
context.actors.any? do |actor|
Flipper.group(name).match?(actor, context)
end
end
end

Expand Down
9 changes: 4 additions & 5 deletions lib/flipper/gates/percentage_of_actors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,12 @@ def enabled?(value)
SCALING_FACTOR = 1_000
private_constant :SCALING_FACTOR

# Internal: Checks if the gate is open for a thing.
# Internal: Checks if the gate is open for one or more actors.
#
# Returns true if gate open for thing, false if not.
# Returns true if gate open for any actors, false if not.
def open?(context)
return false if context.thing.nil?

id = "#{context.feature_name}#{context.thing.value}"
return false unless context.actors?
id = "#{context.feature_name}#{context.actors.map(&:value).sort.join}"
Zlib.crc32(id) % (100 * SCALING_FACTOR) < context.values.percentage_of_actors * SCALING_FACTOR
end

Expand Down
4 changes: 2 additions & 2 deletions lib/flipper/identifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ module Flipper
# end
#
# user = User.new(99)
# Flipper.enable :thing, user
# Flipper.enabled? :thing, user #=> true
# Flipper.enable :some_feature, user
# Flipper.enabled? :some_feature, user #=> true
#
module Identifier
def flipper_id
Expand Down
Loading