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:
parent
2d1b8d49e9
commit
0849ccb1a1
3
Gemfile
3
Gemfile
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
--color
|
||||
--format progress
|
|
@ -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'
|
|
@ -0,0 +1 @@
|
|||
require "bundler/gem_tasks"
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
require "cassandra-cql"
|
||||
require "benchmark"
|
||||
|
||||
module CanvasCassandra
|
||||
require "canvas_cassandra/database"
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue