Class: FluidFeatures::AppState
- Inherits:
-
Object
- Object
- FluidFeatures::AppState
- Defined in:
- lib/fluidfeatures/app/state.rb
Constant Summary collapse
- USER_ID_NUMERIC =
/^\d+$/
- ETAG_WAIT =
Request to FluidFeatures API to long-poll for max 30 seconds. The API may choose a different duration. If not change in this time, API will return HTTP 304.
ENV["FF_DEV"] ? 5 : 30
- WAIT_BETWEEN_FETCH_SUCCESS =
Hard max of 2 req/sec
0.5
- WAIT_BETWEEN_SEND_SUCCESS_NEXT_WAITING =
Hard max of 10 req/sec
0.1
- WAIT_BETWEEN_FETCH_FAILURES =
If we are failing to communicate with the FluidFeautres API then wait for this long between requests.
5
Instance Attribute Summary collapse
-
#app ⇒ Object
Returns the value of attribute app.
Instance Method Summary collapse
- #configure(app) ⇒ Object
- #feature_version_enabled_for_user(feature_name, version_name, user_id, user_attributes = {}) ⇒ Object
- #features ⇒ Object
- #features=(f) ⇒ Object
- #features_lock_synchronize ⇒ Object
- #features_storage ⇒ Object
-
#initialize(app) ⇒ AppState
constructor
seconds.
- #load_state(use_cache = true) ⇒ Object
- #run_loop ⇒ Object
- #run_loop_iteration(wait_between_fetch_success, wait_between_fetch_failures) ⇒ Object
- #start_receiving ⇒ Object
- #stop_receiving(wait = false) ⇒ Object
Constructor Details
#initialize(app) ⇒ AppState
seconds
31 32 33 34 35 |
# File 'lib/fluidfeatures/app/state.rb', line 31 def initialize(app) raise "app invalid : #{app}" unless app.is_a? ::FluidFeatures::App @receiving = false configure(app) end |
Instance Attribute Details
#app ⇒ Object
Returns the value of attribute app.
12 13 14 |
# File 'lib/fluidfeatures/app/state.rb', line 12 def app @app end |
Instance Method Details
#configure(app) ⇒ Object
37 38 39 40 41 |
# File 'lib/fluidfeatures/app/state.rb', line 37 def configure(app) @app = app @features = nil @features_lock = ::Mutex.new end |
#feature_version_enabled_for_user(feature_name, version_name, user_id, user_attributes = {}) ⇒ Object
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/fluidfeatures/app/state.rb', line 159 def feature_version_enabled_for_user(feature_name, version_name, user_id, user_attributes={}) raise "feature_name invalid : #{feature_name}" unless feature_name.is_a? String version_name ||= ::FluidFeatures::DEFAULT_VERSION_NAME raise "version_name invalid : #{version_name}" unless version_name.is_a? String user_attributes ||= {} user_attributes["user"] = user_id.to_s if user_id.is_a? Integer user_id_hash = user_id elsif USER_ID_NUMERIC.match(user_id) user_id_hash = user_id.to_i else user_id_hash = Digest::SHA1.hexdigest(user_id)[-10, 10].to_i(16) end enabled = false feature = features[feature_name] return false unless feature version = feature["versions"][version_name] return false unless version modulus = ((user_id_hash - 1) % feature["num_parts"]) + 1 enabled = version["parts"].include? modulus feature["versions"].each_pair do |other_version_name, other_version| if other_version version_attributes = (other_version["enabled"] || {})["attributes"] if version_attributes user_attributes.each_pair do |attr_key, attr_id| version_attribute = version_attributes[attr_key.to_s] if version_attribute and version_attribute.include? attr_id.to_s if other_version_name == version_name # explicitly enabled for this version return true else # explicitly enabled for another version return false end end end end end end enabled end |
#features ⇒ Object
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/fluidfeatures/app/state.rb', line 60 def features f = nil if @receiving # use features loaded in background features_lock_synchronize do f = @features end end unless f # we have not loaded features yet. # load in foreground but do not use caching (etags) success, state = load_state(use_cache=false) if success unless state # Since we did not use etag caching, state should never # be nil if success was true. raise FFeaturesAppStateLoadFailure.new("Unexpected nil state returned from successful load_state(use_cache=false).") end self.features = f = state else # fluidfeatures API must be down. # load persisted features from disk. self.features = f = features_storage.list end end # we should never return nil unless f # If we still could not load state then croak raise FFeaturesAppStateLoadFailure.new("Could not load features state from API: #{state}") end unless @receiving # start background receiver loop start_receiving end f end |
#features=(f) ⇒ Object
97 98 99 100 101 102 103 104 |
# File 'lib/fluidfeatures/app/state.rb', line 97 def features= f return unless f.is_a? Hash features_lock_synchronize do features_storage.replace(f) @features = f end f end |
#features_lock_synchronize ⇒ Object
207 208 209 210 211 |
# File 'lib/fluidfeatures/app/state.rb', line 207 def features_lock_synchronize @features_lock.synchronize do yield end end |
#features_storage ⇒ Object
56 57 58 |
# File 'lib/fluidfeatures/app/state.rb', line 56 def features_storage @features_storage ||= FluidFeatures::Persistence::Features.create(FluidFeatures.config["cache"]) end |
#load_state(use_cache = true) ⇒ Object
146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/fluidfeatures/app/state.rb', line 146 def load_state(use_cache=true) success, state = app.get("/features", { :verbose => true, :etag_wait => ETAG_WAIT }, use_cache) if success and state state.each_pair do |feature_name, feature| feature["versions"].each_pair do |version_name, version| # convert parts to a Set for quick lookup version["parts"] = Set.new(version["parts"] || []) end end end return success, state end |
#run_loop ⇒ Object
106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/fluidfeatures/app/state.rb', line 106 def run_loop return unless @receiving return if @loop_thread and @loop_thread.alive? @loop_thread = Thread.new do while @receiving run_loop_iteration(WAIT_BETWEEN_FETCH_SUCCESS, WAIT_BETWEEN_FETCH_FAILURES) end end end |
#run_loop_iteration(wait_between_fetch_success, wait_between_fetch_failures) ⇒ Object
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/fluidfeatures/app/state.rb', line 118 def run_loop_iteration(wait_between_fetch_success, wait_between_fetch_failures) begin success, state = load_state # Note, success could be true, but state might be nil. # This occurs with 304 (no change) if success and state # switch out current state with new one self.features = state elsif not success # If service is down, then slow our requests # within this thread sleep wait_between_fetch_failures end # What ever happens never make more than N requests # per second sleep wait_between_fetch_success rescue Exception => err # catch errors, so that we do not affect the rest of the application app.logger.error "load_state failed : #{err.}\n#{err.backtrace.join("\n")}" # hold off for a little while and try again sleep wait_between_fetch_failures end end |
#start_receiving ⇒ Object
43 44 45 46 47 |
# File 'lib/fluidfeatures/app/state.rb', line 43 def start_receiving return if @receiving @receiving = true run_loop end |
#stop_receiving(wait = false) ⇒ Object
49 50 51 52 53 54 |
# File 'lib/fluidfeatures/app/state.rb', line 49 def stop_receiving(wait=false) @receiving = false if wait @loop_thread.join if @loop_thread and @loop_thread.alive? end end |