extract canvas_cassandra gem

The gem still lives in the canvas-lms repo, but has separate tests, and
knows nothing about Canvas proper. Canvas' usage of the gem is through
Canvas::Cassandra::DatabaseBuilder (and Canvas::Cassandra::Migration).

Change-Id: I51878055647225755d54edc5595b8189b7bea3cc
Signed-off-by: Stephan Hagemann <stephan.hagemann@instructure.com>
Reviewed-on: https://gerrit.instructure.com/28632
Reviewed-by: Brian Palmer <brianp@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
Product-Review: Brian Palmer <brianp@instructure.com>
This commit is contained in:
Brian Palmer 2014-01-15 14:03:44 -07:00
parent 2d1b8d49e9
commit 0849ccb1a1
23 changed files with 471 additions and 300 deletions

View File

@ -222,7 +222,8 @@ group :redis do
end
group :cassandra do
gem 'cassandra-cql', '1.2.1', :github => 'kreynolds/cassandra-cql', :ref => 'd100be075b04153cf4116da7512892a1e8c0a7e4'
gem 'cassandra-cql', '1.2.1', :github => 'kreynolds/cassandra-cql', :ref => 'd100be075b04153cf4116da7512892a1e8c0a7e4' #dependency of canvas_cassandra
gem "canvas_cassandra", path: "gems/canvas_cassandra"
end
group :embedly do

View File

@ -22,7 +22,7 @@ class AuditorApiController < ApplicationController
private
def check_configured
not_found unless Canvas::Cassandra::Database.configured?('auditors')
not_found unless Canvas::Cassandra::DatabaseBuilder.configured?('auditors')
end
def query_options(account=nil)

17
gems/canvas_cassandra/.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
*.gem
*.rbc
.bundle
.config
.yardoc
Gemfile.lock
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
tmp

View File

@ -0,0 +1,2 @@
--color
--format progress

View File

@ -0,0 +1,6 @@
source 'https://rubygems.org'
# Specify your gem's dependencies in canvas_cassandra.gemspec
gemspec
gem 'cassandra-cql', '1.2.1', :github => 'kreynolds/cassandra-cql', :ref => 'd100be075b04153cf4116da7512892a1e8c0a7e4'

View File

@ -0,0 +1 @@
require "bundler/gem_tasks"

View File

@ -0,0 +1,24 @@
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
Gem::Specification.new do |spec|
spec.name = "canvas_cassandra"
spec.version = "0.0.1"
spec.authors = ["Brian Palmer"]
spec.email = ["brianp@instructure.com"]
spec.summary = %q{Cassandra wrapper for Canvas LMS}
spec.homepage = "https://github.com/instructure/canvas-lms"
spec.license = "AGPL"
spec.files = `git ls-files`.split($/)
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ["lib"]
spec.add_dependency 'cassandra-cql', '1.2.1'
spec.add_development_dependency "bundler", "~> 1.5"
spec.add_development_dependency "rake"
spec.add_development_dependency "rspec"
end

View File

@ -0,0 +1,6 @@
require "cassandra-cql"
require "benchmark"
module CanvasCassandra
require "canvas_cassandra/database"
end

View File

@ -16,53 +16,21 @@
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
module Canvas::Cassandra
module CanvasCassandra
class Database
def self.configured?(config_name, environment = :current)
raise ArgumentError, "config name required" if config_name.blank?
config = Setting.from_config('cassandra', environment).try(:[], config_name)
config && config['servers'] && config['keyspace']
end
def initialize(fingerprint, servers, opts, logger)
thrift_opts = {}
thrift_opts[:retries] = opts.delete('retries')
thrift_opts[:connect_timeout] = opts.delete('connect_timeout')
thrift_opts[:timeout] = opts.delete('timeout')
def self.from_config(config_name, environment = :current)
@connections ||= {}
environment = Rails.env if environment == :current
key = [config_name, environment]
@connections.fetch(key) do
config = Setting.from_config('cassandra', environment).try(:[], config_name)
unless config
@connections[key] = nil
return nil
end
servers = Array(config['servers'])
raise "No Cassandra servers defined for: #{config_name.inspect}" unless servers.present?
keyspace = config['keyspace']
raise "No keyspace specified for: #{config_name.inspect}" unless keyspace.present?
opts = {:keyspace => keyspace, :cql_version => '3.0.0'}
thrift_opts = {}
thrift_opts[:retries] = config['retries'] if config['retries']
thrift_opts[:connect_timeout] = config['connect_timeout'] if config['connect_timeout']
thrift_opts[:timeout] = config['timeout'] if config['timeout']
@connections[key] = self.new(config_name, environment, servers, opts, thrift_opts)
end
end
def self.config_names
Setting.from_config('cassandra').try(:keys) || []
end
def initialize(cluster_name, environment, servers, opts, thrift_opts)
Bundler.require 'cassandra'
@db = CassandraCQL::Database.new(servers, opts, thrift_opts)
@cluster_name = cluster_name
@environment = environment
@fingerprint = fingerprint
@logger = logger
end
def fingerprint
"#@cluster_name:#@environment"
end
attr_reader :db
attr_reader :db, :fingerprint
# This just takes a raw query string, and params to replace `?` with.
# Though cassandra isn't relational, it'd still be useful to be able to
@ -70,10 +38,11 @@ module Canvas::Cassandra
# or arel is flexible enough for this, rather than rolling our own.
def execute(query, *args)
result = nil
ms = Benchmark.ms do
ms = 1000 * Benchmark.realtime do
result = @db.execute(query, *args)
end
Rails.logger.debug(" #{"CQL (%.2fms)" % [ms]} #{sanitize(query, args)} [#{fingerprint}]")
@logger.debug(" #{"CQL (%.2fms)" % [ms]} #{sanitize(query, args)} [#{fingerprint}]")
result
end
@ -95,20 +64,20 @@ module Canvas::Cassandra
statements = send("#{field}statements")
args = send("#{field}args")
case statements.size
when 0
raise "Cannot execute an empty batch"
when 1
statements + args
else
# http://www.datastax.com/docs/1.1/references/cql/BATCH
# note there's no semicolons between statements in the batch
cql = []
cql << "BEGIN #{'COUNTER ' if field == 'counter_'}BATCH"
cql.concat statements
cql << "APPLY BATCH"
# join with spaces rather than newlines, because cassandra doesn't care
# and syslog doesn't like newlines
[cql.join(" ")] + args
when 0
raise "Cannot execute an empty batch"
when 1
statements + args
else
# http://www.datastax.com/docs/1.1/references/cql/BATCH
# note there's no semicolons between statements in the batch
cql = []
cql << "BEGIN #{'COUNTER ' if field == 'counter_'}BATCH"
cql.concat statements
cql << "APPLY BATCH"
# join with spaces rather than newlines, because cassandra doesn't care
# and syslog doesn't like newlines
[cql.join(" ")] + args
end
end
end
@ -217,9 +186,15 @@ module Canvas::Cassandra
protected
def stringify_hash(hash)
hash.dup.tap do |new_hash|
new_hash.keys.each { |k| new_hash[k.to_s] = new_hash.delete(k) unless k.is_a?(String) }
end
end
def do_update_record(table_name, primary_key_attrs, changes, ttl_seconds)
primary_key_attrs = primary_key_attrs.with_indifferent_access
changes = changes.with_indifferent_access
primary_key_attrs = stringify_hash(primary_key_attrs)
changes = stringify_hash(changes)
where_clause, where_args = build_where_conditions(primary_key_attrs)
primary_key_attrs.each do |key,value|
@ -229,18 +204,18 @@ module Canvas::Cassandra
end
deletes, updates = changes.
# normalize the values since we accept two formats
map { |key,val| [key.to_s, val.is_a?(Array) ? val.last : val] }.
# reject values that are part of the primary key, since those are in the where clause
reject { |key,val| primary_key_attrs.key?(key) }.
# sort, just so the generated cql is deterministic
sort_by(&:first).
# split changes into updates and deletes
partition { |key,val| val.nil? }
# normalize the values since we accept two formats
map { |key,val| [key, val.is_a?(Array) ? val.last : val] }.
# reject values that are part of the primary key, since those are in the where clause
reject { |key,val| primary_key_attrs.key?(key) }.
# sort, just so the generated cql is deterministic
sort_by(&:first).
# split changes into updates and deletes
partition { |key,val| val.nil? }
# inserts and updates in cassandra are equivalent,
# so no need to differentiate here
if updates.present?
if updates && !updates.empty?
args = []
statement = "UPDATE #{table_name}"
if ttl_seconds
@ -253,7 +228,7 @@ module Canvas::Cassandra
update(statement, *args)
end
if deletes.present?
if deletes && !deletes.empty?
args = []
delete_cql = deletes.map(&:first).join(", ")
statement = "DELETE #{delete_cql} FROM #{table_name} WHERE #{where_clause}"
@ -262,22 +237,4 @@ module Canvas::Cassandra
end
end
end
module Migration
module ClassMethods
def cassandra
@cassandra ||= Canvas::Cassandra::Database.from_config(cassandra_cluster)
end
def runnable?
raise "cassandra_cluster is required to be defined" unless respond_to?(:cassandra_cluster) && cassandra_cluster.present?
Shard.current == Shard.birth && Canvas::Cassandra::Database.configured?(cassandra_cluster)
end
end
def self.included(migration)
migration.tag :cassandra
migration.extend ClassMethods
end
end
end

View File

@ -0,0 +1,191 @@
#
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require "spec_helper"
describe CanvasCassandra do
let(:db) do
CanvasCassandra::Database.allocate.tap do |db|
db.send(:instance_variable_set, :@db, double())
db.stub(:sanitize).and_return("")
end
end
describe "#batch" do
it "should do nothing for empty batches" do
db.should_receive(:execute).never
db.in_batch?.should == false
db.batch do
db.in_batch?.should == true
end
db.in_batch?.should == false
end
it "should do update statements in a batch" do
db.should_receive(:execute).with("1")
db.batch { db.update("1") }
db.should_receive(:execute).with("BEGIN BATCH UPDATE ? ? UPDATE ? ? APPLY BATCH", 1, 2, 3, 4)
db.batch { db.update("UPDATE ? ?", 1, 2); db.update("UPDATE ? ?", 3, 4) }
end
it "should not batch up execute statements" do
db.should_receive(:execute).with("SELECT").and_return("RETURN")
db.should_receive(:execute).with("BEGIN BATCH 1 2 APPLY BATCH")
db.batch do
db.update("1")
db.execute("SELECT").should == "RETURN"
db.update("2")
end
end
it "should allow nested batch calls" do
db.should_receive(:execute).with("BEGIN BATCH 1 2 APPLY BATCH")
db.batch do
db.update("1")
db.batch do
db.in_batch?.should == true
db.update("2")
end
end
db.in_batch?.should == false
end
it "should clean up from exceptions" do
db.should_receive(:execute).once.with("2")
begin
db.batch do
db.update("1")
raise "oh noes"
end
rescue
db.in_batch?.should == false
end
db.batch do
db.update("2")
end
end
it "should batch counter calls separately for cql3" do
db.db.stub(:use_cql3?).and_return(true)
db.should_receive(:execute).with("BEGIN BATCH 1 2 APPLY BATCH").once
db.should_receive(:execute).with("BEGIN COUNTER BATCH 3 4 APPLY BATCH").once
db.batch do
db.update("1")
db.update("2")
db.update_counter("3")
db.update_counter("4")
end
end
it "should not batch counter calls separately for older cassandra" do
db.db.stub(:use_cql3?).and_return(false)
db.should_receive(:execute).with("BEGIN BATCH 1 2 APPLY BATCH").once
db.batch do
db.update("1")
db.update_counter("2")
end
end
end
describe "#build_where_conditions" do
it "should build a where clause given a hash" do
db.build_where_conditions(name: "test1").should == ["name = ?", ["test1"]]
db.build_where_conditions(state: "ut", name: "test1").should == ["name = ? AND state = ?", ["test1", "ut"]]
end
end
describe "#update_record" do
it "should do nothing if there are no updates or deletes" do
db.should_receive(:execute).never
db.update_record("test_table", {:id => 5}, {})
end
it "should do lone updates" do
db.should_receive(:execute).with("UPDATE test_table SET name = ? WHERE id = ?", "test", 5)
db.update_record("test_table", {:id => 5}, {:name => "test"})
db.should_receive(:execute).with("UPDATE test_table SET name = ? WHERE id = ?", "test", 5)
db.update_record("test_table", {:id => 5}, {:name => [nil, "test"]})
end
it "should do multi-updates" do
db.should_receive(:execute).with("UPDATE test_table SET name = ?, nick = ? WHERE id = ?", "test", "new", 5)
db.update_record("test_table", {:id => 5}, {:name => "test", :nick => ["old", "new"]})
end
it "should do lone deletes" do
db.should_receive(:execute).with("DELETE name FROM test_table WHERE id = ?", 5)
db.update_record("test_table", {:id => 5}, {:name => nil})
db.should_receive(:execute).with("DELETE name FROM test_table WHERE id = ?", 5)
db.update_record("test_table", {:id => 5}, {:name => ["old", nil]})
end
it "should do multi-deletes" do
db.should_receive(:execute).with("DELETE name, nick FROM test_table WHERE id = ?", 5)
db.update_record("test_table", {:id => 5}, {:name => nil, :nick => ["old", nil]})
end
it "should do combined updates and deletes" do
db.should_receive(:execute).with("BEGIN BATCH UPDATE test_table SET name = ? WHERE id = ? DELETE nick FROM test_table WHERE id = ? APPLY BATCH", "test", 5, 5)
db.update_record("test_table", {:id => 5}, {:name => "test", :nick => nil})
end
it "should work when already in a batch" do
db.should_receive(:execute).with("BEGIN BATCH UPDATE ? UPDATE test_table SET name = ? WHERE id = ? DELETE nick FROM test_table WHERE id = ? UPDATE ? APPLY BATCH", 1, "test", 5, 5, 2)
db.batch do
db.update("UPDATE ?", 1)
db.update_record("test_table", {:id => 5}, {:name => "test", :nick => nil})
db.update("UPDATE ?", 2)
end
end
it "should handle compound primary keys" do
db.should_receive(:execute).with("UPDATE test_table SET name = ? WHERE id = ? AND sub_id = ?", "test", 5, "sub!")
db.update_record("test_table", {:id => 5, :sub_id => "sub!"}, {:name => "test", :id => 5, :sub_id => [nil, "sub!"]})
end
it "should disallow changing a primary key component" do
expect {
db.update_record("test_table", {:id => 5, :sub_id => "sub!"}, {:name => "test", :id => 5, :sub_id => ["old", "sub!"]})
}.to raise_error(ArgumentError)
end
end
describe "#insert_record" do
it "constructs correct queries when the params are strings" do
db.should_receive(:execute).with("UPDATE test_table SET name = ? WHERE id = ?", "test", 5)
db.insert_record("test_table", {'id' => 5}, {'name' => "test"})
end
it "constructs correct queries when the params are symbols" do
db.should_receive(:execute).with("UPDATE test_table SET name = ? WHERE id = ?", "test", 5)
db.insert_record("test_table", {:id => 5}, {:name => "test"})
end
it "should not update given nil values in an AR#attributes style hash" do
db.should_receive(:execute).with("UPDATE test_table SET name = ? WHERE id = ?", "test", 5)
db.insert_record("test_table", {:id => 5}, {:name => "test", :nick => nil})
end
it "should not update given nil values in an AR#changes style hash" do
db.should_receive(:execute).with("UPDATE test_table SET name = ? WHERE id = ?", "test", 5)
db.insert_record("test_table", {:id => 5}, {:name => [nil, "test"], :nick => [nil, nil]})
end
end
end

View File

@ -0,0 +1,56 @@
#
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require "spec_helper"
class TestLogger
def debug(*args)
end
end
describe "execute and update" do
let(:db) do
# TODO: Setting.from_config really deserves to be its own Config component that we could use here
config_path = File.expand_path("../../../../../config/cassandra.yml", __FILE__)
test_config = YAML.load(ERB.new(File.read(config_path)).result)['test']['page_views']
CanvasCassandra::Database.new("test_conn", test_config['servers'], {keyspace: test_config['keyspace'], cql_version: '3.0.0'}, TestLogger.new)
end
before do
begin
db.execute("drop table page_views")
rescue CassandraCQL::Error::InvalidRequestException
end
db.execute("create table page_views (request_id text primary key, user_id bigint)")
end
after do
db.execute("drop table page_views")
end
it "should return the result from execute" do
db.execute("select count(*) from page_views").fetch['count'].should == 0
db.select_value("select count(*) from page_views").should == 0
db.execute("insert into page_views (request_id, user_id) values (?, ?)", "test", 0).should == nil
end
it "should return nothing from update" do
db.update("select count(*) from page_views").should == nil
db.update("insert into page_views (request_id, user_id) values (?, ?)", "test", 0).should == nil
end
end

View File

@ -0,0 +1,39 @@
#
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This file was generated by the `rspec --init` command. Conventionally, all
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
# Require this file using `require "spec_helper"` to ensure that it is only
# loaded once.
#
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
require "canvas_cassandra"
require "yaml"
RSpec.configure do |config|
config.treat_symbols_as_metadata_keys_with_true_values = true
config.run_all_when_everything_filtered = true
config.filter_run :focus
# Run specs in random order to surface order dependencies. If you find an
# order dependency and want to debug it, you can fix the order by providing
# the seed, which is printed after each run.
# --seed 1234
config.order = 'random'
end

View File

@ -0,0 +1,10 @@
# This is simply a backwards-compatibility shim, once plugins are all updated
# we can delete this file
module Canvas::Cassandra
module Database
class << self
delegate :configured?, :from_config, :config_names, to: '::Canvas::Cassandra::DatabaseBuilder'
end
end
end

View File

@ -0,0 +1,38 @@
module Canvas
module Cassandra
module DatabaseBuilder
def self.configured?(config_name, environment = :current)
raise ArgumentError, "config name required" if config_name.blank?
config = Setting.from_config('cassandra', environment)
config = config && config[config_name]
config && config['servers'] && config['keyspace']
end
def self.from_config(config_name, environment = :current)
@connections ||= {}
environment = Rails.env if environment == :current
key = [config_name, environment]
@connections.fetch(key) do
config = Setting.from_config('cassandra', environment)
config = config && config[config_name]
unless config
@connections[key] = nil
return nil
end
servers = Array(config['servers'])
raise "No Cassandra servers defined for: #{config_name.inspect}" unless servers.present?
keyspace = config['keyspace']
raise "No keyspace specified for: #{config_name.inspect}" unless keyspace.present?
opts = {:keyspace => keyspace, :cql_version => '3.0.0'}
fingerprint = "#{config_name}:#{environment}"
Bundler.require 'cassandra'
@connections[key] = CanvasCassandra::Database.new(fingerprint, servers, opts, Rails.logger)
end
end
def self.config_names
Setting.from_config('cassandra').try(:keys) || []
end
end
end
end

View File

@ -0,0 +1,21 @@
module Canvas
module Cassandra
module Migration
module ClassMethods
def cassandra
@cassandra ||= Canvas::Cassandra::DatabaseBuilder.from_config(cassandra_cluster)
end
def runnable?
raise "cassandra_cluster is required to be defined" unless respond_to?(:cassandra_cluster) && cassandra_cluster.present?
Shard.current == Shard.birth && Canvas::Cassandra::DatabaseBuilder.configured?(cassandra_cluster)
end
end
def self.included(migration)
migration.tag :cassandra
migration.extend ClassMethods
end
end
end
end

View File

@ -31,11 +31,11 @@ class EventStream
end
def database
Canvas::Cassandra::Database.from_config(database_name)
Canvas::Cassandra::DatabaseBuilder.from_config(database_name)
end
def available?
Canvas::Cassandra::Database.configured?(database_name)
Canvas::Cassandra::DatabaseBuilder.configured?(database_name)
end
def on_insert(&callback)

View File

@ -210,8 +210,8 @@ namespace :db do
queue = config['queue']
drop_database(queue) if queue rescue nil
drop_database(config) rescue nil
Canvas::Cassandra::Database.config_names.each do |cass_config|
db = Canvas::Cassandra::Database.from_config(cass_config)
Canvas::Cassandra::DatabaseBuilder.config_names.each do |cass_config|
db = Canvas::Cassandra::DatabaseBuilder.from_config(cass_config)
db.tables.each do |table|
db.execute("DROP TABLE #{table}")
end

View File

@ -22,7 +22,7 @@ require File.expand_path(File.dirname(__FILE__) + '/../../cassandra_spec_helper'
describe "AuthenticationAudit API", type: :request do
context "not configured" do
before do
Canvas::Cassandra::Database.stubs(:configured?).with('auditors').returns(false)
Canvas::Cassandra::DatabaseBuilder.stubs(:configured?).with('auditors').returns(false)
site_admin_user(user: user_with_pseudonym(account: Account.site_admin))
end

View File

@ -22,7 +22,7 @@ require File.expand_path(File.dirname(__FILE__) + '/../../cassandra_spec_helper'
describe "GradeChangeAudit API", type: :request do
context "not configured" do
before do
Canvas::Cassandra::Database.stubs(:configured?).with('auditors').returns(false)
Canvas::Cassandra::DatabaseBuilder.stubs(:configured?).with('auditors').returns(false)
user_with_pseudonym(account: Account.default)
@user.account_users.create(account: Account.default)
end

View File

@ -20,7 +20,7 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
shared_examples_for "cassandra page views" do
before do
if Canvas::Cassandra::Database.configured?('page_views')
if Canvas::Cassandra::DatabaseBuilder.configured?('page_views')
Setting.set('enable_page_views', 'cassandra')
else
pending "needs cassandra page_views configuration"
@ -30,7 +30,7 @@ end
shared_examples_for "cassandra audit logs" do
before do
unless Canvas::Cassandra::Database.configured?('auditors')
unless Canvas::Cassandra::DatabaseBuilder.configured?('auditors')
pending "needs cassandra auditors configuration"
end
end

View File

@ -1,198 +0,0 @@
#
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require File.expand_path(File.dirname(__FILE__) + '/../../cassandra_spec_helper.rb')
describe "Canvas::Redis::Cassandra" do
let(:db) do
Canvas::Cassandra::Database.allocate.tap do |db|
db.send(:instance_variable_set, :@db, mock())
db.stubs(:sanitize).returns("")
end
end
describe "#batch" do
it "should do nothing for empty batches" do
db.expects(:execute).never
db.in_batch?.should == false
db.batch do
db.in_batch?.should == true
end
db.in_batch?.should == false
end
it "should do update statements in a batch" do
db.expects(:execute).with("1")
db.batch { db.update("1") }
db.expects(:execute).with("BEGIN BATCH UPDATE ? ? UPDATE ? ? APPLY BATCH", 1, 2, 3, 4)
db.batch { db.update("UPDATE ? ?", 1, 2); db.update("UPDATE ? ?", 3, 4) }
end
it "should not batch up execute statements" do
db.expects(:execute).with("SELECT").returns("RETURN")
db.expects(:execute).with("BEGIN BATCH 1 2 APPLY BATCH")
db.batch do
db.update("1")
db.execute("SELECT").should == "RETURN"
db.update("2")
end
end
it "should allow nested batch calls" do
db.expects(:execute).with("BEGIN BATCH 1 2 APPLY BATCH")
db.batch do
db.update("1")
db.batch do
db.in_batch?.should == true
db.update("2")
end
end
db.in_batch?.should == false
end
it "should clean up from exceptions" do
db.expects(:execute).never
begin
db.batch do
db.update("1")
raise "oh noes"
end
rescue
db.in_batch?.should == false
end
db.expects(:execute).with("2")
db.batch do
db.update("2")
end
end
it "should batch counter calls separately for cql3" do
db.db.stubs(:use_cql3?).returns(true)
db.expects(:execute).with("BEGIN BATCH 1 2 APPLY BATCH").once
db.expects(:execute).with("BEGIN COUNTER BATCH 3 4 APPLY BATCH").once
db.batch do
db.update("1")
db.update("2")
db.update_counter("3")
db.update_counter("4")
end
end
it "should not batch counter calls separately for older cassandra" do
db.db.stubs(:use_cql3?).returns(false)
db.expects(:execute).with("BEGIN BATCH 1 2 APPLY BATCH").once
db.batch do
db.update("1")
db.update_counter("2")
end
end
end
describe "#build_where_conditions" do
it "should build a where clause given a hash" do
db.build_where_conditions(name: "test1").should == ["name = ?", ["test1"]]
db.build_where_conditions(state: "ut", name: "test1").should == ["name = ? AND state = ?", ["test1", "ut"]]
end
end
describe "#update_record" do
it "should do nothing if there are no updates or deletes" do
db.expects(:execute).never
db.update_record("test_table", { :id => 5 }, {})
end
it "should do lone updates" do
db.expects(:execute).with("UPDATE test_table SET name = ? WHERE id = ?", "test", 5)
db.update_record("test_table", { :id => 5 }, { :name => "test" })
db.expects(:execute).with("UPDATE test_table SET name = ? WHERE id = ?", "test", 5)
db.update_record("test_table", { :id => 5 }, { :name => [nil, "test"] })
end
it "should do multi-updates" do
db.expects(:execute).with("UPDATE test_table SET name = ?, nick = ? WHERE id = ?", "test", "new", 5)
db.update_record("test_table", { :id => 5 }, { :name => "test", :nick => ["old", "new"] })
end
it "should do lone deletes" do
db.expects(:execute).with("DELETE name FROM test_table WHERE id = ?", 5)
db.update_record("test_table", { :id => 5 }, { :name => nil })
db.expects(:execute).with("DELETE name FROM test_table WHERE id = ?", 5)
db.update_record("test_table", { :id => 5 }, { :name => ["old", nil] })
end
it "should do multi-deletes" do
db.expects(:execute).with("DELETE name, nick FROM test_table WHERE id = ?", 5)
db.update_record("test_table", { :id => 5 }, { :name => nil, :nick => ["old", nil] })
end
it "should do combined updates and deletes" do
db.expects(:execute).with("BEGIN BATCH UPDATE test_table SET name = ? WHERE id = ? DELETE nick FROM test_table WHERE id = ? APPLY BATCH", "test", 5, 5)
db.update_record("test_table", { :id => 5 }, { :name => "test", :nick => nil })
end
it "should work when already in a batch" do
db.expects(:execute).with("BEGIN BATCH UPDATE ? UPDATE test_table SET name = ? WHERE id = ? DELETE nick FROM test_table WHERE id = ? UPDATE ? APPLY BATCH", 1, "test", 5, 5, 2)
db.batch do
db.update("UPDATE ?", 1)
db.update_record("test_table", { :id => 5 }, { :name => "test", :nick => nil })
db.update("UPDATE ?", 2)
end
end
it "should handle compound primary keys" do
db.expects(:execute).with("UPDATE test_table SET name = ? WHERE id = ? AND sub_id = ?", "test", 5, "sub!")
db.update_record("test_table", { :id => 5, :sub_id => "sub!" }, { :name => "test", :id => 5, :sub_id => [nil, "sub!"] })
end
it "should disallow changing a primary key component" do
expect {
db.update_record("test_table", { :id => 5, :sub_id => "sub!" }, { :name => "test", :id => 5, :sub_id => ["old", "sub!"]})
}.to raise_error(ArgumentError)
end
end
describe "#insert_record" do
it "should not delete nils in an AR#attributes style hash" do
db.expects(:execute).with("UPDATE test_table SET name = ? WHERE id = ?", "test", 5)
db.insert_record("test_table", { :id => 5 }, { :name => "test", :nick => nil })
end
it "should not delete nils in an AR#changes style hash" do
db.expects(:execute).with("UPDATE test_table SET name = ? WHERE id = ?", "test", 5)
db.insert_record("test_table", { :id => 5 }, { :name => [nil, "test"], :nick => [nil, nil] })
end
end
describe "execute and update" do
it_should_behave_like "cassandra page views"
let(:db) { PageView::EventStream.database }
it "should return the result from execute" do
db.execute("select count(*) from page_views").fetch['count'].should == 0
db.select_value("select count(*) from page_views").should == 0
db.execute("insert into page_views (request_id, user_id) values (?, ?)", "test", 0).should == nil
end
it "should return nothing from update" do
db.update("select count(*) from page_views").should == nil
db.update("insert into page_views (request_id, user_id) values (?, ?)", "test", 0).should == nil
end
end
end

View File

@ -26,8 +26,8 @@ describe EventStream do
def @database.update_record(*args); end
def @database.insert_record(*args); end
def @database.update(*args); end
::Canvas::Cassandra::Database.stubs(:from_config).with(@database_name.to_s).returns(@database)
::Canvas::Cassandra::Database.stubs(:configured?).with(@database_name.to_s).returns(true)
::Canvas::Cassandra::DatabaseBuilder.stubs(:from_config).with(@database_name.to_s).returns(@database)
::Canvas::Cassandra::DatabaseBuilder.stubs(:configured?).with(@database_name.to_s).returns(true)
end
context "setup block" do

View File

@ -116,8 +116,8 @@ def truncate_all_tables
end
def truncate_all_cassandra_tables
Canvas::Cassandra::Database.config_names.each do |cass_config|
db = Canvas::Cassandra::Database.from_config(cass_config)
Canvas::Cassandra::DatabaseBuilder.config_names.each do |cass_config|
db = Canvas::Cassandra::DatabaseBuilder.from_config(cass_config)
db.tables.each do |table|
db.execute("TRUNCATE #{table}")
end