1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 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 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 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 145 146 147 148 149 150 151 152 153 154 155 156 157 158 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
|
require "socket"
require "flipper/adapters/http"
require "flipper/adapters/poll"
require "flipper/poller"
require "flipper/adapters/memory"
require "flipper/adapters/dual_write"
require "flipper/adapters/sync/synchronizer"
require "flipper/cloud/instrumenter"
require "brow"
module Flipper
module Cloud
class Configuration
# The set of valid ways that syncing can happpen.
VALID_SYNC_METHODS = Set[
:poll,
:webhook,
].freeze
DEFAULT_URL = "https://www.flippercloud.io/adapter".freeze
# Private: Keeps track of brow instances so they can be shared across
# threads.
def self.brow_instances
@brow_instances ||= Concurrent::Map.new
end
# Public: The token corresponding to an environment on flippercloud.io.
attr_accessor :token
# Public: The url for http adapter. Really should only be customized for
# development work. Feel free to forget you ever saw this.
attr_reader :url
# Public: net/http read timeout for all http requests (default: 5).
attr_accessor :read_timeout
# Public: net/http open timeout for all http requests (default: 5).
attr_accessor :open_timeout
# Public: net/http write timeout for all http requests (default: 5).
attr_accessor :write_timeout
# Public: IO stream to send debug output too. Off by default.
#
# # for example, this would send all http request information to STDOUT
# configuration = Flipper::Cloud::Configuration.new
# configuration.debug_output = STDOUT
attr_accessor :debug_output
# Public: Instrumenter to use for the Flipper instance returned by
# Flipper::Cloud.new (default: Flipper::Instrumenters::Noop).
#
# # for example, to use active support notifications you could do:
# configuration = Flipper::Cloud::Configuration.new
# configuration.instrumenter = ActiveSupport::Notifications
attr_accessor :instrumenter
# Public: Local adapter that all reads should go to in order to ensure
# latency is low and resiliency is high. This adapter is automatically
# kept in sync with cloud.
#
# # for example, to use active record you could do:
# configuration = Flipper::Cloud::Configuration.new
# configuration.local_adapter = Flipper::Adapters::ActiveRecord.new
attr_accessor :local_adapter
# Public: The Integer or Float number of seconds between attempts to bring
# the local in sync with cloud (default: 10).
attr_accessor :sync_interval
# Public: The secret used to verify if syncs in the middleware should
# occur or not.
attr_accessor :sync_secret
def initialize(options = {})
@token = options.fetch(:token) { ENV["FLIPPER_CLOUD_TOKEN"] }
if @token.nil?
raise ArgumentError, "Flipper::Cloud token is missing. Please set FLIPPER_CLOUD_TOKEN or provide the token (e.g. Flipper::Cloud.new(token: 'token'))."
end
@read_timeout = options.fetch(:read_timeout) { ENV.fetch("FLIPPER_CLOUD_READ_TIMEOUT", 5).to_f }
@open_timeout = options.fetch(:open_timeout) { ENV.fetch("FLIPPER_CLOUD_OPEN_TIMEOUT", 5).to_f }
@write_timeout = options.fetch(:write_timeout) { ENV.fetch("FLIPPER_CLOUD_WRITE_TIMEOUT", 5).to_f }
@sync_interval = options.fetch(:sync_interval) { ENV.fetch("FLIPPER_CLOUD_SYNC_INTERVAL", 10).to_f }
@sync_secret = options.fetch(:sync_secret) { ENV["FLIPPER_CLOUD_SYNC_SECRET"] }
@local_adapter = options.fetch(:local_adapter) { Adapters::Memory.new }
@debug_output = options[:debug_output]
@adapter_block = ->(adapter) { adapter }
self.url = options.fetch(:url) { ENV.fetch("FLIPPER_CLOUD_URL", DEFAULT_URL) }
instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
# This is alpha. Don't use this unless you are me. And you are not me.
cloud_instrument = options.fetch(:cloud_instrument) { ENV["FLIPPER_CLOUD_INSTRUMENT"] == "1" }
@instrumenter = if cloud_instrument
Instrumenter.new(brow: brow, instrumenter: instrumenter)
else
instrumenter
end
end
# Public: Read or customize the http adapter. Calling without a block will
# perform a read. Calling with a block yields the cloud adapter
# for customization.
#
# # for example, to instrument the http calls, you can wrap the http
# # adapter with the intsrumented adapter
# configuration = Flipper::Cloud::Configuration.new
# configuration.adapter do |adapter|
# Flipper::Adapters::Instrumented.new(adapter)
# end
#
def adapter(&block)
if block_given?
@adapter_block = block
else
@adapter_block.call app_adapter
end
end
# Public: Set url for the http adapter.
attr_writer :url
def sync
Flipper::Adapters::Sync::Synchronizer.new(local_adapter, http_adapter, {
instrumenter: instrumenter,
interval: sync_interval,
}).call
end
def brow
self.class.brow_instances.compute_if_absent(url + token) do
uri = URI.parse(url)
uri.path = "#{uri.path}/events".squeeze("/")
Brow::Client.new({
url: uri.to_s,
headers: {
"Accept" => "application/json",
"Content-Type" => "application/json",
"User-Agent" => "Flipper v#{VERSION} via Brow v#{Brow::VERSION}",
"Flipper-Cloud-Token" => @token,
}
})
end
end
# Public: The method that will be used to synchronize local adapter with
# cloud. (default: :poll, will be :webhook if sync_secret is set).
def sync_method
sync_secret ? :webhook : :poll
end
private
def app_adapter
sync_method == :webhook ? dual_write_adapter : poll_adapter
end
def dual_write_adapter
Flipper::Adapters::DualWrite.new(local_adapter, http_adapter)
end
def poller
Flipper::Poller.get(@url + @token, {
interval: sync_interval,
remote_adapter: http_adapter,
instrumenter: instrumenter,
}).tap(&:start)
end
def poll_adapter
Flipper::Adapters::Poll.new(poller, dual_write_adapter)
end
def http_adapter
Flipper::Adapters::Http.new({
url: @url,
read_timeout: @read_timeout,
open_timeout: @open_timeout,
write_timeout: @write_timeout,
max_retries: 0, # we'll handle retries ourselves
debug_output: @debug_output,
headers: {
"Flipper-Cloud-Token" => @token,
},
})
end
end
end
end
|