From a71cb38224c7ab0321d3a3be421c26f3ff1095b1 Mon Sep 17 00:00:00 2001 From: Phill Baker Date: Thu, 26 May 2016 22:38:44 -0400 Subject: [PATCH] wip For #295 --- lib/vanity.rb | 1 + lib/vanity/adapters.rb | 3 +- lib/vanity/adapters/abstract_adapter.rb | 17 ++ lib/vanity/adapters/active_record_adapter.rb | 29 +- lib/vanity/adapters/mock_adapter.rb | 9 +- lib/vanity/adapters/mongodb_adapter.rb | 3 +- lib/vanity/adapters/redis_adapter.rb | 18 +- lib/vanity/connection.rb | 40 ++- lib/vanity/frameworks/rails.rb | 3 +- lib/vanity/playground.rb | 2 +- lib/vanity/vanity.rb | 299 ++++++++++--------- test/adapters/redis_adapter_test.rb | 16 +- test/connection_test.rb | 10 +- test/frameworks/rails/rails_test.rb | 6 +- test/test_helper.rb | 2 +- test/vanity_test.rb | 27 +- 16 files changed, 301 insertions(+), 184 deletions(-) diff --git a/lib/vanity.rb b/lib/vanity.rb index 75b55243..afd9c076 100644 --- a/lib/vanity.rb +++ b/lib/vanity.rb @@ -14,6 +14,7 @@ # @see Vanity::Configuration # @see Vanity::Connection module Vanity + class InvalidSpecification < StandardError; end end require "vanity/version" diff --git a/lib/vanity/adapters.rb b/lib/vanity/adapters.rb index 1fee89f5..c0cd4031 100644 --- a/lib/vanity/adapters.rb +++ b/lib/vanity/adapters.rb @@ -2,10 +2,11 @@ module Vanity module Adapters class << self # Creates new connection to underlying datastore and returns suitable - # adapter (adapter object extends AbstractAdapter and wraps the + # adapter (adapter objects extend AbstractAdapter and wrap the # connection). # # @since 1.4.0 + # @deprecated def establish_connection(spec) begin require "vanity/adapters/#{spec[:adapter]}_adapter" diff --git a/lib/vanity/adapters/abstract_adapter.rb b/lib/vanity/adapters/abstract_adapter.rb index fe94d7ea..41197d06 100644 --- a/lib/vanity/adapters/abstract_adapter.rb +++ b/lib/vanity/adapters/abstract_adapter.rb @@ -3,12 +3,29 @@ module Adapters # Base class for all adapters. Adapters wrap underlying connection to a # datastore and implement an API that Vanity can use to store/access # metrics, experiments, etc. + # + # The `initialize` method of subclasses should accept a hash of connection + # parameters (the keys are guaranteed to be symbols). All dependencies + # should be `required` in the initialization method - allowing version + # checking at runtime. class AbstractAdapter # Returns true if connected. def active? false end + # Hook for using an adapter that requires schema changes, useful when + # integrating into a framework that provides schema management, e.g. + # Rails' `db:migrate`. + def connect_on_schema_change? + false + end + + # Open connection. + # @since 2.2.2 + def connect! + end + # Close connection, release any resources. def disconnect! end diff --git a/lib/vanity/adapters/active_record_adapter.rb b/lib/vanity/adapters/active_record_adapter.rb index 5d5a3c9c..39f4fcb1 100644 --- a/lib/vanity/adapters/active_record_adapter.rb +++ b/lib/vanity/adapters/active_record_adapter.rb @@ -2,6 +2,7 @@ module Vanity module Adapters class << self # Creates new ActiveRecord connection and returns ActiveRecordAdapter. + # @deprecated def active_record_connection(spec) require "active_record" ActiveRecordAdapter.new(spec) @@ -102,10 +103,13 @@ def self.retrieve(experiment, identity, create = true, update_with = nil) end def initialize(options) - @options = options.inject({}) { |h,kv| h[kv.first.to_s] = kv.last ; h } - if @options["active_record_adapter"] && (@options["active_record_adapter"] != "default") - @options["adapter"] = @options["active_record_adapter"] - VanityRecord.establish_connection(@options) + require "active_record" + @options = options.clone + if @options[:active_record_adapter] && (@options[:active_record_adapter] != "default") + @options[:adapter] = @options.delete(:active_record_adapter) + else + @options.delete(:adapter) + @options.delete(:active_record_adapter) end end @@ -113,12 +117,27 @@ def active? VanityRecord.connected? && VanityRecord.connection.active? end + # ActiveRecord adapter should connect when doing database migrations. + def connect_on_schema_change? + true + end + + def connect! + if @options.empty? + # VanityRecord.connection.reconnect! + # do nothing, already connected + else + VanityRecord.establish_connection(@options) + end + end + def disconnect! VanityRecord.connection.disconnect! if active? end def reconnect! - VanityRecord.connection.reconnect! + disconnect! + connect! end def flushdb diff --git a/lib/vanity/adapters/mock_adapter.rb b/lib/vanity/adapters/mock_adapter.rb index 3754532c..45e29361 100644 --- a/lib/vanity/adapters/mock_adapter.rb +++ b/lib/vanity/adapters/mock_adapter.rb @@ -4,6 +4,7 @@ class << self # Creates and returns new MockAdapter. # # @since 1.4.0 + # @deprecated def mock_connection(spec) MockAdapter.new(spec) end @@ -15,14 +16,18 @@ def mock_connection(spec) # @since 1.4.0 class MockAdapter < AbstractAdapter def initialize(options) - @metrics = @@metrics ||= {} - @experiments = @@experiments ||= {} + # No-op end def active? !!@metrics end + def connect! + @metrics = @@metrics ||= {} + @experiments = @@experiments ||= {} + end + def disconnect! @metrics = nil @experiments = nil diff --git a/lib/vanity/adapters/mongodb_adapter.rb b/lib/vanity/adapters/mongodb_adapter.rb index 52cade48..b4a8cbd2 100644 --- a/lib/vanity/adapters/mongodb_adapter.rb +++ b/lib/vanity/adapters/mongodb_adapter.rb @@ -4,6 +4,7 @@ class << self # Creates new connection to MongoDB and returns MongoAdapter. # # @since 1.4.0 + # @deprecated def mongo_connection(spec) require "mongo" MongodbAdapter.new(spec) @@ -18,9 +19,9 @@ class MongodbAdapter < AbstractAdapter attr_reader :mongo def initialize(options) + require "mongo" @options = options.clone @options[:database] ||= (@options[:path] && @options[:path].split("/")[1]) || "vanity" - connect! end def active? diff --git a/lib/vanity/adapters/redis_adapter.rb b/lib/vanity/adapters/redis_adapter.rb index 65a8bc0a..ba0bf85e 100644 --- a/lib/vanity/adapters/redis_adapter.rb +++ b/lib/vanity/adapters/redis_adapter.rb @@ -4,6 +4,7 @@ class << self # Creates new connection to Redis and returns RedisAdapter. # # @since 1.4.0 + # @deprecated def redis_connection(spec) require "redis" fail "redis >= 2.1 is required" unless valid_redis_version? @@ -13,12 +14,14 @@ def redis_connection(spec) RedisAdapter.new(spec) end + # @deprecated def valid_redis_version? Gem.loaded_specs['redis'].version >= Gem::Version.create('2.1') end + # @deprecated def valid_redis_namespace_version? - Gem.loaded_specs['redis'].version >= Gem::Version.create('1.1.0') + Gem.loaded_specs['redis-namespace'].version >= Gem::Version.create('1.1.0') end end @@ -26,13 +29,24 @@ def valid_redis_namespace_version? # # @since 1.4.0 class RedisAdapter < AbstractAdapter + MINIMUM_REDIS_GEM = Gem::Version.create('2.1') + MINIMUM_REDIS_NAMESPACE_GEM = Gem::Version.create('1.1.0') + attr_reader :redis def initialize(options) + require "redis" + require "redis/namespace" + + valid_redis = Gem.loaded_specs['redis'].version >= MINIMUM_REDIS_GEM + valid_redis_namespace = Gem.loaded_specs['redis-namespace'].version >= MINIMUM_REDIS_NAMESPACE_GEM + + fail "redis >= 2.1 is required" unless valid_redis + fail "redis-namespace >= 1.1.0 is required" unless valid_redis_namespace + @options = options.clone @options[:db] ||= @options[:database] || (@options[:path] && @options.delete(:path).split("/")[1].to_i) @options[:thread_safe] = true - connect! end def active? diff --git a/lib/vanity/connection.rb b/lib/vanity/connection.rb index f87acc79..f60f3ab6 100644 --- a/lib/vanity/connection.rb +++ b/lib/vanity/connection.rb @@ -1,4 +1,7 @@ module Vanity + # @deprecated This class is a facade into one of the adapters in the + # Vanity::Adapters namspace, it will be merged into the top level + # Vanity.connect! helper and/or the adapters. class Connection class InvalidSpecification < StandardError; end @@ -33,9 +36,8 @@ class InvalidSpecification < StandardError; end def initialize(specification=nil) @specification = specification || DEFAULT_SPECIFICATION - if Autoconnect.playground_should_autoconnect? - @adapter = setup_connection(@specification) - end + @adapter = adapter_from_specification(@specification) + connect! end # Closes the current connection. @@ -45,6 +47,13 @@ def disconnect! @adapter.disconnect! if connected? end + # Creates a connection. + # + # @since 2.2.2 + def connect! + @adapter && @adapter.connect! + end + # Returns true if connection is open. # # @since 2.0.0 @@ -54,27 +63,27 @@ def connected? private - def setup_connection(spec) + def adapter_from_specification(spec) case spec when String - spec_hash = build_specification_hash_from_url(spec) - establish_connection(spec_hash) + spec_hash = specification_hash_from_url(spec) + initialize_adapter(spec_hash) when Hash validate_specification_hash(spec) if spec[:redis] - establish_connection( + initialize_adapter( adapter: :redis, redis: spec[:redis] ) else - establish_connection(spec) + initialize_adapter(spec) end else raise InvalidSpecification.new("Unsupported connection specification: #{spec.inspect}") end end - def build_specification_hash_from_url(connection_url) + def specification_hash_from_url(connection_url) uri = URI.parse(connection_url) params = CGI.parse(uri.query) if uri.query { @@ -93,8 +102,17 @@ def validate_specification_hash(spec) raise InvalidSpecification unless all_symbol_keys end - def establish_connection(spec) - Adapters.establish_connection(spec) + def initialize_adapter(spec) + begin + require "vanity/adapters/#{spec[:adapter]}_adapter" + rescue LoadError + raise "Could not find #{spec[:adapter]} in your load path" + end + + klass = spec[:adapter].to_s.split('_').collect(&:capitalize).join + # Get the class constant directly from the module instead of chaining + # the constant with `::` to avoid breaking on jruby in 1 + Vanity::Adapters.const_get("#{klass}Adapter").new(spec) end end end \ No newline at end of file diff --git a/lib/vanity/frameworks/rails.rb b/lib/vanity/frameworks/rails.rb index 9569517d..ab1e06bd 100644 --- a/lib/vanity/frameworks/rails.rb +++ b/lib/vanity/frameworks/rails.rb @@ -9,7 +9,8 @@ def self.load! # Do this at the very end of initialization, allowing you to change # connection adapter, turn collection on/off, etc. ::Rails.configuration.after_initialize do - Vanity.load! if Vanity.connection.connected? + Vanity.connection # connect if necessary + Vanity.playground # load if necessary end end diff --git a/lib/vanity/playground.rb b/lib/vanity/playground.rb index 1df1257c..fbcdcf15 100644 --- a/lib/vanity/playground.rb +++ b/lib/vanity/playground.rb @@ -219,7 +219,7 @@ def connection # @deprecated # @see Vanity.connection def connected? - Vanity.connection.connected? + Vanity.connection && Vanity.connection.connected? end # @since 1.4.0 diff --git a/lib/vanity/vanity.rb b/lib/vanity/vanity.rb index a43a75fc..b8470b7e 100644 --- a/lib/vanity/vanity.rb +++ b/lib/vanity/vanity.rb @@ -4,177 +4,194 @@ module Vanity extend Vanity::Helpers - # Returns the current configuration. - # - # @see Vanity::Configuration - # @since 2.0.0 - def self.configuration(set_if_needed=true) - if defined?(@configuration) && @configuration - @configuration - elsif set_if_needed - configure! - end - end - - # @since 2.0.0 - def self.configure! - @configuration = Configuration.new - end + DEFAULT_SPECIFICATION = { adapter: "redis" } - # @since 2.0.0 - def self.reset! - @configuration = nil - configuration - end + class< mocked_redis) + adapter.connect! + assert_equal mocked_redis, adapter.redis + end + + it "connects to existing redis" do + mocked_redis = stub("Redis") + adapter = Vanity::Adapters::RedisAdapter.new(:redis => mocked_redis) + adapter.connect! assert_equal mocked_redis, adapter.redis end diff --git a/test/connection_test.rb b/test/connection_test.rb index 5c797069..df21ebcf 100644 --- a/test/connection_test.rb +++ b/test/connection_test.rb @@ -1,14 +1,16 @@ require "test_helper" +require "vanity/adapters/redis_adapter" +require "vanity/adapters/mock_adapter" describe Vanity::Connection do describe "#new" do it "establishes connection with default specification" do - Vanity::Adapters.expects(:establish_connection).with(adapter: "redis") + Vanity::Adapters::RedisAdapter.expects(:new).with(adapter: "redis") Vanity::Connection.new end it "establishes connection given a connection specification" do - Vanity::Adapters.expects(:establish_connection).with(adapter: "mock") + Vanity::Adapters::MockAdapter.expects(:new).with(adapter: "mock") Vanity::Connection.new(adapter: "mock") end @@ -19,7 +21,7 @@ end it "parses from a string" do - Vanity::Adapters.expects(:establish_connection).with( + Vanity::Adapters::RedisAdapter.expects(:new).with( adapter: 'redis', username: 'user', password: 'secrets', @@ -39,7 +41,7 @@ it "allows a redis connection to be specified" do redis = stub("Redis") - Vanity::Adapters.expects(:establish_connection).with(adapter: :redis, redis: redis) + Vanity::Adapters::RedisAdapter.expects(:new).with(adapter: :redis, redis: redis) Vanity::Connection.new(redis: redis) end end diff --git a/test/frameworks/rails/rails_test.rb b/test/frameworks/rails/rails_test.rb index 3e5607f8..409076e3 100644 --- a/test/frameworks/rails/rails_test.rb +++ b/test/frameworks/rails/rails_test.rb @@ -29,8 +29,8 @@ RB end - it "connection from string" do - assert_equal "redis://192.168.1.1:6379/5", load_rails(%Q{\nVanity.playground.establish_connection "redis://192.168.1.1:6379/5"\n}, <<-RB) + it "connection programatically from string" do + assert_equal "redis://192.168.1.1:6379/5", load_rails(%Q{\nVanity.playground.establish_connection("redis://192.168.1.1:6379/5")\n}, <<-RB) $stdout << Vanity.playground.connection RB end @@ -186,7 +186,7 @@ adapter: mock YML end - assert_equal "false", load_rails("", <<-RB) + assert_equal "false", load_rails("", <<-RB, "production") $stdout << Vanity.playground.collecting? RB ensure diff --git a/test/test_helper.rb b/test/test_helper.rb index 8679f229..88951b3d 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -84,7 +84,7 @@ def vanity_reset # Call this on teardown. It wipes put the playground and any state held in it # (mostly experiments), resets vanity ID, and clears database of all experiments. def nuke_playground - Vanity.playground.connection.flushdb + Vanity.playground.connection.flushdb if Vanity.connection(false) && Vanity.connection.connected? new_playground end diff --git a/test/vanity_test.rb b/test/vanity_test.rb index d4caa987..275bff63 100644 --- a/test/vanity_test.rb +++ b/test/vanity_test.rb @@ -59,11 +59,11 @@ describe "#connect!" do it "returns a connection" do - assert_kind_of Vanity::Connection, Vanity.connect! + assert_kind_of Vanity::Connection, Vanity.connect!("mock:/") end it "returns a new connection" do - refute_same Vanity.connect!, Vanity.connect! + refute_same Vanity.connect!("mock:/"), Vanity.connect!("mock:/") end describe "deprecated settings" do @@ -131,7 +131,24 @@ end describe "#reconnect!" do - it "reconnects with the same configuration" do + before do + FakeFS.activate! + + connection_config = VanityTestHelpers::VANITY_CONFIGS["vanity.yml.mock"] + + FileUtils.mkpath "./config" + File.open("./config/vanity.yml", "w") do |f| + f.write(connection_config) + end + Vanity.reset! + end + + after do + FakeFS.deactivate! + FakeFS::FileSystem.clear + end + + it "reconnects with the same configuration from the config file" do Vanity.disconnect! original_specification = Vanity.connection.specification Vanity.reconnect! @@ -139,8 +156,8 @@ end it "creates a new connection" do - original_configuration = Vanity.connection - refute_same original_configuration, Vanity.reconnect! + original_connection = Vanity.connection + refute_same original_connection, Vanity.reconnect! end end