From 81e05038bc51bf2565f7429d90744d687f74e8bb Mon Sep 17 00:00:00 2001 From: Evan Prothro Date: Tue, 26 Apr 2016 12:56:57 -0500 Subject: [PATCH] add consistency option, logging support and execution refactor --- README.md | 2 + cassie.gemspec | 2 +- lib/cassie/connection_handler/cluster.rb | 6 + lib/cassie/queries/README.md | 56 ++++++++ .../queries/logging/cql_execution_event.rb | 10 +- lib/cassie/queries/statement.rb | 22 +--- lib/cassie/queries/statement/consistency.rb | 33 +++++ lib/cassie/queries/statement/execution.rb | 41 ++++++ lib/cassie/testing/fake/execution_info.rb | 2 +- .../logging/cql_execution_event_spec.rb | 6 +- .../queries/statement/consistency_spec.rb | 120 ++++++++++++++++++ .../queries/statement/execution_spec.rb | 42 ++++++ 12 files changed, 318 insertions(+), 24 deletions(-) create mode 100644 lib/cassie/queries/statement/consistency.rb create mode 100644 lib/cassie/queries/statement/execution.rb create mode 100644 spec/lib/cassie/queries/statement/consistency_spec.rb create mode 100644 spec/lib/cassie/queries/statement/execution_spec.rb diff --git a/README.md b/README.md index 80a27db..a6a62cf 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,8 @@ class UserByUsernameQuery < Cassie::Query select :users_by_username where :username, :eq + + consistency :quorum end ``` diff --git a/cassie.gemspec b/cassie.gemspec index d5bb69d..7b83ed2 100644 --- a/cassie.gemspec +++ b/cassie.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'cassie' - s.version = '1.0.0.alpha.17' + s.version = '1.0.0.alpha.19' s.summary = "Apache Cassandra application support" s.description = <<-EOS.strip.gsub(/\s+/, ' ') Cassie provides database configration, versioned migrations, diff --git a/lib/cassie/connection_handler/cluster.rb b/lib/cassie/connection_handler/cluster.rb index 96fe16c..ec571b9 100644 --- a/lib/cassie/connection_handler/cluster.rb +++ b/lib/cassie/connection_handler/cluster.rb @@ -1,6 +1,12 @@ require 'benchmark' module Cassie::ConnectionHandler + # ## Cassie::ConnectionHandler::Cluster + # + # Adds cluster instance configuration and memoization. + # + # Include in any class or module that responds to `configuration` with + # a cassandra cluster options hash. module Cluster def cluster diff --git a/lib/cassie/queries/README.md b/lib/cassie/queries/README.md index b3cd63e..f01c7de 100644 --- a/lib/cassie/queries/README.md +++ b/lib/cassie/queries/README.md @@ -148,6 +148,62 @@ or end ``` +#### Consistency configuration + +The [consistency level](http://datastax.github.io/ruby-driver/v2.1.6/api/cassandra/#consistencies-constant) for a query is determined by your `Cassie::configuration` by default, falling to back to the `Cassandra` default if none is given. + +```ruby +Cassie.configuration[:consistency] +=> nil + +Cassie.cluster.instance_variable_get(:@execution_options).consistency +=> :one +``` + +A Cassie::Query looks for a consistency level defined on the object, subclass, then base class levels. If one is found, it will override the `Cassandra` default when the query is executed. + +```ruby + select :posts_by_author_category + + where :author_id, :eq + where :category, :eq, if: :filter_by_category? + + def filter_by_category? + #true or false, as makes sense for your query + end + + def consistency + #dynamically determine a query object's consistency level + if filter_by_category? + :quorum + else + super + end + end +``` + +```ruby + select :posts_by_author_category + + where :author_id, :eq + where :category, :eq + + consistency :quorum +``` + +```ruby +# lib/tasks/interesting_task.rake +require_relative "interesting_worker" + +task :interesting_task do + Cassandra::Query.consistency = :all + + InterestingWorker.new.perform +end + + +``` + #### Object Mapping For Selection Queries, resources are returned as structs by default for manipulation using accessor methods. diff --git a/lib/cassie/queries/logging/cql_execution_event.rb b/lib/cassie/queries/logging/cql_execution_event.rb index 38a8520..978bf62 100644 --- a/lib/cassie/queries/logging/cql_execution_event.rb +++ b/lib/cassie/queries/logging/cql_execution_event.rb @@ -9,7 +9,7 @@ def duration # in milliseconds end def message - color "(#{duration.round(1)}ms) #{statement}" + color "(#{duration.round(1)}ms) #{statement} [#{consistency}]" end protected @@ -29,6 +29,14 @@ def statement end end + def consistency + if execution_info + execution_info.consistency + else + "consistency level unknown" + end + end + def traced? execution_info && !!trace end diff --git a/lib/cassie/queries/statement.rb b/lib/cassie/queries/statement.rb index 3d8abe9..dd32325 100644 --- a/lib/cassie/queries/statement.rb +++ b/lib/cassie/queries/statement.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/string/filters' require 'active_support/hash_with_indifferent_access' +require_relative 'statement/execution' require_relative 'statement/preparation' require_relative 'statement/callbacks' require_relative 'statement/limiting' @@ -16,6 +17,7 @@ module Statement extend ::ActiveSupport::Concern included do + include Execution include Preparation include Callbacks include Limiting @@ -37,13 +39,6 @@ def table self.class.table end - # Executes the statment, populates result - # returns true or false indicating a successful execution or not - def execute - @result = session.execute(statement) - execution_successful? - end - # returns a CQL string, or a Cassandra::Statement # that is ready for execution def statement @@ -60,19 +55,6 @@ def build_cql_and_bindings end end - def execution_successful? - #TODO: rethink this, it knows too much - raise "execution not complete, no results to parse" unless result - - # empty select - return true if result.empty? - - # failed upsert - return false if (!result.rows.first["[applied]"].nil?) && (result.rows.first["[applied]"] == false) - - true - end - private def eval_if_opt?(value) diff --git a/lib/cassie/queries/statement/consistency.rb b/lib/cassie/queries/statement/consistency.rb new file mode 100644 index 0000000..bd6b9ac --- /dev/null +++ b/lib/cassie/queries/statement/consistency.rb @@ -0,0 +1,33 @@ +module Cassie::Queries::Statement + module Consistency + extend ActiveSupport::Concern + + included do + attr_writer :consistency + end + + module ClassMethods + def inherited(subclass) + subclass.consistency = consistency + super + end + + def consistency=(val) + @consistency = val + end + + def consistency(val=:get) + if val == :get + @consistency if defined?(@consistency) + else + self.consistency = val + end + end + end + + def consistency + return @consistency if defined?(@consistency) + self.class.consistency + end + end +end \ No newline at end of file diff --git a/lib/cassie/queries/statement/execution.rb b/lib/cassie/queries/statement/execution.rb new file mode 100644 index 0000000..f0865f4 --- /dev/null +++ b/lib/cassie/queries/statement/execution.rb @@ -0,0 +1,41 @@ +require_relative 'consistency' + +module Cassie::Queries::Statement + module Execution + extend ActiveSupport::Concern + + included do + include Consistency + end + + # Executes the statment, populates result + # returns true or false indicating a successful execution or not + def execute + @result = session.execute(statement, execution_options) + execution_successful? + end + + def execution_options + {}.tap do |opts| + #TODO: rework consistency module to be more + # abstract implementation for all execution options + opts[:consistency] = consistency if consistency + end + end + + protected + + def execution_successful? + #TODO: rethink this, it knows too much + raise "execution not complete, no results to parse" unless result + + # empty select + return true if result.empty? + + # failed upsert + return false if (!result.rows.first["[applied]"].nil?) && (result.rows.first["[applied]"] == false) + + true + end + end +end \ No newline at end of file diff --git a/lib/cassie/testing/fake/execution_info.rb b/lib/cassie/testing/fake/execution_info.rb index fd83908..2be3a24 100644 --- a/lib/cassie/testing/fake/execution_info.rb +++ b/lib/cassie/testing/fake/execution_info.rb @@ -1,6 +1,6 @@ module Cassie::Testing::Fake class ExecutionInfo - attr_reader :statement + attr_reader :statement, :consistency def initialize(statement) @statement = statement diff --git a/spec/lib/cassie/queries/logging/cql_execution_event_spec.rb b/spec/lib/cassie/queries/logging/cql_execution_event_spec.rb index 7f38cea..d248c48 100644 --- a/spec/lib/cassie/queries/logging/cql_execution_event_spec.rb +++ b/spec/lib/cassie/queries/logging/cql_execution_event_spec.rb @@ -16,9 +16,10 @@ let(:duration_sec){ duration_ms / 1000.0 } let(:duration_ms){ 1.5 } let(:payload){ {execution_info: execution_info} } - let(:execution_info) { double(statement: statement, trace: nil) } + let(:execution_info) { double(statement: statement, consistency: consistency, trace: nil) } let(:statement){ Cassandra::Statements::Simple.new(cql) } let(:cql){ 'some CQL' } + let(:consistency){ 'some consistency level' } describe "#message" do it "includes the duration" do @@ -27,6 +28,9 @@ it "includes the statement" do expect(object.message).to include(cql) end + it "includes the consistency level" do + expect(object.message).to include(consistency) + end end end diff --git a/spec/lib/cassie/queries/statement/consistency_spec.rb b/spec/lib/cassie/queries/statement/consistency_spec.rb new file mode 100644 index 0000000..a9732b9 --- /dev/null +++ b/spec/lib/cassie/queries/statement/consistency_spec.rb @@ -0,0 +1,120 @@ +RSpec.describe Cassie::Queries::Statement::Consistency do + let(:base_class)do + Class.new do + include Cassie::Queries::Statement::Consistency + end + end + let(:subclass){ Class.new(base_class) } + let(:object) { subclass.new } + let(:consistency){ Cassandra::CONSISTENCIES.sample } + # let(:alt_consistency){ (Cassandra::CONSISTENCIES - [alt_consistency]).sample } + let(:default_consistency){ nil } + + # before(:each){ @original = base_class.consistency } + # after(:each){ base_class.consistency = @original } + describe "BaseClass" do + describe ".consistency" do + it "defaults to nil" do + expect(base_class.consistency).to eq(default_consistency) + end + + context "when set" do + it "overrides default" do + base_class.consistency = consistency + expect(base_class.consistency).to eq(consistency) + end + end + end + end + + describe "SubClass" do + describe ".consistency" do + it "defaults to base default" do + expect(subclass.consistency).to eq(default_consistency) + end + + context "when base class has been set" do + before(:each) { base_class.consistency = consistency } + + it "inherits base class setting" do + expect(subclass.consistency).to eq(consistency) + end + end + + context "when set with setter" do + before(:each){ subclass.consistency = consistency } + + it "overrides default" do + expect(subclass.consistency).to eq(consistency) + end + it "doesn't change base class" do + expect(base_class.consistency).to eq(default_consistency) + end + end + + context "when set with getter for DSL feel" do + before(:each){ subclass.consistency(consistency) } + + it "overrides default" do + expect(subclass.consistency).to eq(consistency) + end + it "doesn't change base class" do + expect(base_class.consistency).to eq(default_consistency) + end + end + + context "when overwritten" do + let(:consistency){ :three } + let(:subclass) do + Class.new(base_class) do + def self.consistency + :three + end + end + end + + it "overrides base class setting" do + expect(subclass.consistency).to eq(consistency) + end + it "doesn't change base class" do + expect(base_class.consistency).to eq(default_consistency) + end + end + end + + describe "#consistency" do + it "defaults to base_class value" do + expect(object.consistency).to eq(default_consistency) + end + + context "when set" do + before(:each){ object.consistency = consistency } + + it "overrides default" do + expect(object.consistency).to eq(consistency) + end + it "doesn't change subclass" do + expect(subclass.consistency).to eq(default_consistency) + end + it "doesn't change base class" do + expect(base_class.consistency).to eq(default_consistency) + end + + context "when .consistency overwritten" do + let(:consistency){ :three } + let(:subclass) do + Class.new(base_class) do + def self.consistency + :three + end + end + end + + it "uses object value" do + expect(object.consistency).to eq(consistency) + end + end + end + end + end +end diff --git a/spec/lib/cassie/queries/statement/execution_spec.rb b/spec/lib/cassie/queries/statement/execution_spec.rb new file mode 100644 index 0000000..885ce05 --- /dev/null +++ b/spec/lib/cassie/queries/statement/execution_spec.rb @@ -0,0 +1,42 @@ +RSpec.describe Cassie::Queries::Statement::Execution do + let(:base_class){ Cassie::FakeQuery } + let(:klass) do + Class.new(base_class) do + end + end + let(:object) do + klass.new + end + + describe "#execute" do + it "passes the statment and execution_options to Cassandra" do + end + end + + describe "execution_options" do + it "defaults to an empty hash" do + expect(object.execution_options.keys).to be_empty + end + + context "when consistency is defined" do + let(:opt_value){ :three } + before(:each) do + object.consistency = opt_value + end + + it "includes consistency" do + expect(object.execution_options[:consistency]).to eq(opt_value) + end + end + + context "when consistency is not defined" do + before(:each) do + object.consistency = nil + end + + it "does not include consistency" do + expect(object.execution_options.keys).not_to include(:consistency) + end + end + end +end