Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cloud Sync #342

Merged
merged 3 commits into from
Feb 23, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions lib/flipper/adapters/sync.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class Sync
# Public: The name of the adapter.
attr_reader :name

# Public: The synchronizer that will keep the local and remote in sync.
attr_reader :synchronizer

# Public: Build a new sync instance.
#
# local - The local flipper adapter that should serve reads.
Expand Down
4 changes: 4 additions & 0 deletions lib/flipper/adapters/sync/interval_synchronizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ def self.now_ms
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
end

# Public: The number of milliseconds between invocations of the
# wrapped synchronizer.
attr_reader :interval

# Public: Initializes a new interval synchronizer.
#
# synchronizer - The Synchronizer to call when the interval has passed.
Expand Down
48 changes: 38 additions & 10 deletions lib/flipper/cloud/configuration.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
require "flipper/adapters/http"
require "flipper/adapters/memory"
require "flipper/adapters/sync"
require "flipper/instrumenters/noop"

module Flipper
Expand Down Expand Up @@ -36,19 +38,34 @@ class Configuration
# 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: Number of milliseconds between attempts to bring the local in
# sync with cloud (default: 10_000 aka 10 seconds).
attr_accessor :sync_interval

def initialize(options = {})
@token = options.fetch(:token)
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
@read_timeout = options.fetch(:read_timeout, 5)
@open_timeout = options.fetch(:open_timeout, 5)
@sync_interval = options.fetch(:sync_interval, 10_000)
@local_adapter = options.fetch(:local_adapter) { Adapters::Memory.new }
@debug_output = options[:debug_output]
@adapter_block = ->(adapter) { adapter }

self.url = options.fetch(:url, DEFAULT_URL)
end

# Public: Read or customize the http adapter. Calling without a block will
# perform a read. Calling with a block yields the http_adapter
# 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
Expand All @@ -62,23 +79,34 @@ def adapter(&block)
if block_given?
@adapter_block = block
else
@adapter_block.call(http_adapter)
@adapter_block.call sync_adapter
end
end

# Public: Set url and uri for the http adapter.
# Public: Set url for the http adapter.
attr_writer :url

private

def sync_adapter
sync_options = {
instrumenter: instrumenter,
interval: sync_interval,
}
Flipper::Adapters::Sync.new(local_adapter, http_adapter, sync_options)
end

def http_adapter
Flipper::Adapters::Http.new(url: @url,
read_timeout: @read_timeout,
open_timeout: @open_timeout,
debug_output: @debug_output,
headers: {
"Feature-Flipper-Token" => @token,
})
http_options = {
url: @url,
read_timeout: @read_timeout,
open_timeout: @open_timeout,
debug_output: @debug_output,
headers: {
"Feature-Flipper-Token" => @token,
},
}
Flipper::Adapters::Http.new(http_options)
end
end
end
Expand Down
21 changes: 20 additions & 1 deletion spec/flipper/cloud/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,36 @@
expect(instance.open_timeout).to eq(5)
end

it "can set sync_interval" do
instance = described_class.new(required_options.merge(sync_interval: 1_000))
expect(instance.sync_interval).to eq(1_000)
end

it "passes sync_interval into sync adapter" do
# The initial sync of http to local invokes this web request.
stub_request(:get, /featureflipper\.com/).to_return(status: 200, body: "{}")

instance = described_class.new(required_options.merge(sync_interval: 1_000))
expect(instance.adapter.synchronizer.interval).to be(1_000)
end

it "can set debug_output" do
instance = described_class.new(required_options.merge(debug_output: STDOUT))
expect(instance.debug_output).to eq(STDOUT)
end

it "defaults adapter block" do
# The initial sync of http to local invokes this web request.
stub_request(:get, /featureflipper\.com/).to_return(status: 200, body: "{}")

instance = described_class.new(required_options)
expect(instance.adapter).to be_instance_of(Flipper::Adapters::Http)
expect(instance.adapter).to be_instance_of(Flipper::Adapters::Sync)
end

it "can override adapter block" do
# The initial sync of http to local invokes this web request.
stub_request(:get, /featureflipper\.com/).to_return(status: 200, body: "{}")

instance = described_class.new(required_options)
instance.adapter do |adapter|
Flipper::Adapters::Instrumented.new(adapter)
Expand Down
15 changes: 12 additions & 3 deletions spec/flipper/cloud_spec.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
require 'helper'
require 'flipper/cloud'
require 'flipper/adapters/instrumented'
require 'flipper/instrumenters/memory'

RSpec.describe Flipper::Cloud do
before do
stub_request(:get, /featureflipper\.com/).to_return(status: 200, body: "{}")
end

context "initialize with token" do
let(:token) { 'asdf' }

before do
@instance = described_class.new(token)
memoized_adapter = @instance.adapter
@http_adapter = memoized_adapter.adapter
sync_adapter = memoized_adapter.adapter
@http_adapter = sync_adapter.instance_variable_get('@remote')
@http_client = @http_adapter.instance_variable_get('@client')
end

Expand Down Expand Up @@ -40,9 +46,12 @@

context 'initialize with token and options' do
before do
stub_request(:get, /fakeflipper\.com/).to_return(status: 200, body: "{}")

@instance = described_class.new('asdf', url: 'https://www.fakeflipper.com/sadpanda')
memoized_adapter = @instance.adapter
@http_adapter = memoized_adapter.adapter
sync_adapter = memoized_adapter.adapter
@http_adapter = sync_adapter.instance_variable_get('@remote')
@http_client = @http_adapter.instance_variable_get('@client')
end

Expand All @@ -55,7 +64,7 @@
end

it 'can set instrumenter' do
instrumenter = Object.new
instrumenter = Flipper::Instrumenters::Memory.new
instance = described_class.new('asdf', instrumenter: instrumenter)
expect(instance.instrumenter).to be(instrumenter)
end
Expand Down