cassanity 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/.gitignore +2 -1
  2. data/.travis.yml +1 -0
  3. data/Gemfile +3 -0
  4. data/Guardfile +0 -2
  5. data/README.md +11 -0
  6. data/doc/Instrumentation.md +40 -0
  7. data/doc/Migrations.md +132 -0
  8. data/examples/keyspaces.rb +11 -7
  9. data/lib/cassanity/argument_generators/column_family_delete.rb +1 -1
  10. data/lib/cassanity/argument_generators/columns.rb +33 -0
  11. data/lib/cassanity/argument_generators/where_clause.rb +1 -1
  12. data/lib/cassanity/client.rb +3 -1
  13. data/lib/cassanity/column.rb +48 -0
  14. data/lib/cassanity/column_family.rb +21 -2
  15. data/lib/cassanity/connection.rb +4 -8
  16. data/lib/cassanity/error.rb +18 -11
  17. data/lib/cassanity/executors/cassandra_cql.rb +79 -50
  18. data/lib/cassanity/instrumentation/log_subscriber.rb +4 -5
  19. data/lib/cassanity/instrumentation/metriks.rb +6 -0
  20. data/lib/cassanity/instrumentation/metriks_subscriber.rb +16 -0
  21. data/lib/cassanity/instrumentation/statsd.rb +6 -0
  22. data/lib/cassanity/instrumentation/statsd_subscriber.rb +22 -0
  23. data/lib/cassanity/instrumentation/subscriber.rb +58 -0
  24. data/lib/cassanity/instrumenters/memory.rb +0 -1
  25. data/lib/cassanity/keyspace.rb +10 -8
  26. data/lib/cassanity/migration.rb +125 -0
  27. data/lib/cassanity/migration_proxy.rb +76 -0
  28. data/lib/cassanity/migrator.rb +154 -0
  29. data/lib/cassanity/result_transformers/column_families.rb +20 -0
  30. data/lib/cassanity/result_transformers/columns.rb +21 -0
  31. data/lib/cassanity/result_transformers/keyspaces.rb +21 -0
  32. data/lib/cassanity/result_transformers/mirror.rb +1 -1
  33. data/lib/cassanity/result_transformers/result_to_array.rb +1 -1
  34. data/lib/cassanity/retry_strategies/exponential_backoff.rb +43 -0
  35. data/lib/cassanity/retry_strategies/retry_n_times.rb +29 -0
  36. data/lib/cassanity/retry_strategies/retry_strategy.rb +35 -0
  37. data/lib/cassanity/version.rb +1 -1
  38. data/spec/helper.rb +8 -0
  39. data/spec/integration/cassanity/column_family_spec.rb +36 -25
  40. data/spec/integration/cassanity/connection_spec.rb +11 -11
  41. data/spec/integration/cassanity/fixtures/migrations/20130224135000_create_users.rb +17 -0
  42. data/spec/integration/cassanity/fixtures/migrations/20130225135002_create_apps.rb +15 -0
  43. data/spec/integration/cassanity/fixtures/migrations/20130226135004_add_username_to_users.rb +9 -0
  44. data/spec/integration/cassanity/instrumentation/log_subscriber_spec.rb +71 -0
  45. data/spec/integration/cassanity/instrumentation/metriks_subscriber_spec.rb +48 -0
  46. data/spec/integration/cassanity/instrumentation/statsd_subscriber_spec.rb +58 -0
  47. data/spec/integration/cassanity/keyspace_spec.rb +21 -21
  48. data/spec/integration/cassanity/migration_spec.rb +157 -0
  49. data/spec/integration/cassanity/migrator_spec.rb +212 -0
  50. data/spec/support/cassanity_helpers.rb +21 -17
  51. data/spec/support/fake_udp_socket.rb +27 -0
  52. data/spec/unit/cassanity/argument_generators/batch_spec.rb +5 -5
  53. data/spec/unit/cassanity/argument_generators/column_family_delete_spec.rb +20 -6
  54. data/spec/unit/cassanity/argument_generators/column_family_update_spec.rb +6 -6
  55. data/spec/unit/cassanity/argument_generators/columns_spec.rb +45 -0
  56. data/spec/unit/cassanity/argument_generators/keyspace_create_spec.rb +1 -1
  57. data/spec/unit/cassanity/argument_generators/keyspace_drop_spec.rb +1 -1
  58. data/spec/unit/cassanity/argument_generators/keyspace_use_spec.rb +1 -1
  59. data/spec/unit/cassanity/argument_generators/where_clause_spec.rb +2 -2
  60. data/spec/unit/cassanity/client_spec.rb +10 -3
  61. data/spec/unit/cassanity/column_family_spec.rb +20 -3
  62. data/spec/unit/cassanity/column_spec.rb +76 -0
  63. data/spec/unit/cassanity/connection_spec.rb +1 -1
  64. data/spec/unit/cassanity/error_spec.rb +7 -2
  65. data/spec/unit/cassanity/executors/cassandra_cql_spec.rb +76 -23
  66. data/spec/unit/cassanity/keyspace_spec.rb +38 -13
  67. data/spec/unit/cassanity/migration_proxy_spec.rb +81 -0
  68. data/spec/unit/cassanity/migration_spec.rb +12 -0
  69. data/spec/unit/cassanity/migrator_spec.rb +20 -0
  70. data/spec/unit/cassanity/retry_strategies/exponential_backoff_spec.rb +37 -0
  71. data/spec/unit/cassanity/retry_strategies/retry_n_times_spec.rb +47 -0
  72. data/spec/unit/cassanity/retry_strategies/retry_strategy_spec.rb +27 -0
  73. metadata +56 -4
data/.gitignore CHANGED
@@ -7,7 +7,6 @@ Gemfile.lock
7
7
  InstalledFiles
8
8
  _yardoc
9
9
  coverage
10
- doc/
11
10
  lib/bundler/man
12
11
  pkg
13
12
  rdoc
@@ -17,3 +16,5 @@ test/version_tmp
17
16
  tmp
18
17
  bin
19
18
  vendor/gems
19
+ .env
20
+ tags
data/.travis.yml CHANGED
@@ -6,5 +6,6 @@ rvm:
6
6
  notifications:
7
7
  email: false
8
8
  bundler_args: --without guard
9
+ before_script: sudo service cassandra status
9
10
  services:
10
11
  - cassandra
data/Gemfile CHANGED
@@ -1,9 +1,12 @@
1
1
  source 'https://rubygems.org'
2
2
  gemspec
3
3
 
4
+ gem 'dotenv'
4
5
  gem 'rake'
5
6
  gem 'rspec', '~> 2.8'
6
7
  gem 'activesupport', :require => false
8
+ gem 'metriks', :require => false
9
+ gem 'statsd-ruby', :require => false
7
10
 
8
11
  group(:guard) do
9
12
  gem 'guard'
data/Guardfile CHANGED
@@ -7,7 +7,6 @@ guard 'bundler' do
7
7
  end
8
8
 
9
9
  rspec_options = {
10
- version: 2,
11
10
  all_after_pass: false,
12
11
  all_on_start: false,
13
12
  keep_failed: false
@@ -21,4 +20,3 @@ guard 'rspec', rspec_options do
21
20
  ] }
22
21
  watch('spec/helper.rb') { "spec" }
23
22
  end
24
-
data/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  Layer of goodness on top of cassandra-cql so you do not have to write CQL strings all over the place.
4
4
 
5
+ ## Note about Cassandra 1.2
6
+
7
+ At this time, cassandra 1.2 is not supported. Under the hood, cassanity uses [cassandra-cql](https://github.com/kreynolds/cassandra-cql), which does not currently support 1.2. It needs the thrift bindings ([initial pull request](https://github.com/kreynolds/cassandra-cql/pull/39)) updated for 1.2 or to wrap the new binary protocol ([initial pull request](https://github.com/kreynolds/cassandra-cql/pull/40)). I'm hoping to work on this in February or March, but if you want to take a stab, that would be awesome.
8
+
5
9
  ## Installation
6
10
 
7
11
  Add this line to your application's Gemfile:
@@ -99,9 +103,16 @@ You can also do a lot more. Here are a few more [examples](https://github.com/jn
99
103
  * [Column Families](https://github.com/jnunemaker/cassanity/tree/master/examples/column_families.rb)
100
104
  * [Select a Range](https://github.com/jnunemaker/cassanity/tree/master/examples/select_range.rb)
101
105
 
106
+ ## More Reading
107
+
108
+ * [Instrumentation](https://github.com/jnunemaker/cassanity/tree/master/doc/Instrumentation.md)
109
+ * [Migrations](https://github.com/jnunemaker/cassanity/tree/master/doc/Migrations.md)
110
+
102
111
  ## Compatibility
103
112
 
104
113
  * Ruby 1.9.3
114
+ * Cassandra CQL 3.x
115
+ * Any version of cassandra that works with cassandra-cql and supports CQL 3.
105
116
 
106
117
  ## Contributing
107
118
 
@@ -0,0 +1,40 @@
1
+ # Instrumentation
2
+
3
+ Cassanity comes with a log subscriber and automatic metriks or statsd instrumentation.
4
+ By default these work with ActiveSupport::Notifications, but only require the
5
+ pieces of ActiveSupport that are needed and only do so if you actually attempt
6
+ to require the instrumentation files listed below.
7
+
8
+ To use the log subscriber:
9
+
10
+ ```ruby
11
+ # Gemfile
12
+ gem 'activesupport'
13
+
14
+ # config/initializers/cassanity.rb (or wherever you want it)
15
+ require 'cassanity/instrumentation/log_subscriber'
16
+ ```
17
+
18
+ To use the metriks instrumentation:
19
+
20
+ ```ruby
21
+ # Gemfile
22
+ gem 'activesupport'
23
+ gem 'metriks'
24
+
25
+ # config/initializers/cassanity.rb (or wherever you want it)
26
+ require 'cassanity/instrumentation/metriks'
27
+ ```
28
+
29
+ To use the statsd instrumentation:
30
+
31
+ ```ruby
32
+ # Gemfile
33
+ gem 'activesupport'
34
+ gem 'statsd-ruby'
35
+
36
+ # config/initializers/cassanity.rb (or wherever you want it)
37
+ require 'cassanity/instrumentation/statsd'
38
+
39
+ Cassanity::Instrumentation::StatsdSubscriber.client = Statsd.new
40
+ ```
data/doc/Migrations.md ADDED
@@ -0,0 +1,132 @@
1
+ # Migrations
2
+
3
+ Cassanity comes with a migrations framework similar to ActiveRecord. Because not everyone will need migrations, you must require it in addition like so:
4
+
5
+ ```ruby
6
+ require 'cassanity/migrator'
7
+ ```
8
+ ## Creating Your First Migration
9
+
10
+ Out of the box, Cassanity comes with no generators for migrations. Fear not, if you can create a file, you can create a migration. For example, let's say that you would like to create a users column family.
11
+
12
+ First, create the directory that will house your migrations.
13
+
14
+ ```bash
15
+ mkdir -p db/migrate
16
+ ```
17
+
18
+ Second, create a file for the create users migration:
19
+
20
+ ```bash
21
+ touch db/migrate/001_create_users.rb
22
+ ```
23
+
24
+ Now, open the file up in your favorite editor and paste in the following:
25
+
26
+ ```ruby
27
+ class CreateUsers < Cassanity::Migration
28
+ def up
29
+ create_column_family :users, {
30
+ primary_key: :id,
31
+ columns: {
32
+ id: :timeuuid,
33
+ name: :text,
34
+ age: :int,
35
+ updated_at: :timestamp,
36
+ },
37
+ }
38
+ end
39
+
40
+ def down
41
+ drop_column_family :users
42
+ end
43
+ end
44
+ ```
45
+
46
+ If you have ever used Active Record's migrations, the previous migration file should look really familiar. Instead of going for a fancy DSL, like AR's, the convenience methods you get are just a very thing layer on top of Cassanity itself.
47
+
48
+ The available convenience methods are:
49
+
50
+ * `create_column_family(column_family_name, schema)` - aliased to add_column_family, create_table and add_table
51
+ * `drop_column_family(column_family_name)` - aliased to drop_table
52
+ * `add_column(column_family_name, column_name, type)` - shortcut for alter_column_family with add
53
+ * `drop_column(column_family_name, column_name)` - shortcut for alter_column_family with drop
54
+ * `alter_column_family(column_family_name, args)` - args are passed straight through to ColumnFamily#alter.
55
+ * `add_index(column_family_name, column_name, options = {})` - create a cassandra secondary index; aliased to create_index
56
+ * `drop_index(column_family_name, index_name)` - drop a cassandra secondary index
57
+ * `say_with_time(message) { # do something }` - spit a message out to the migrators log, time the duration of the block, and also spit out the duration upon finished execution of the block
58
+ * `say(message)` - spit a message out to the migrator's log
59
+
60
+ You can always check out the [source](https://github.com/jnunemaker/cassanity/blob/master/lib/cassanity/migration.rb) or [specs](https://github.com/jnunemaker/cassanity/blob/master/spec/integration/cassanity/migration_spec.rb) for more about these methods.
61
+
62
+ ## Running migrations
63
+
64
+ The act of running migrations is performed by a migrator. To migrate all pending migrations, simply call migrate:
65
+
66
+ ```ruby
67
+ require 'pathname'
68
+ require 'cassanity/migrator'
69
+
70
+ # Assuming the keyspace is already created.
71
+ keyspace = Cassanity::Client.new[:my_app]
72
+
73
+ # Path to our migrations directory.
74
+ migrations_path = Pathname(__FILE__).dirname.join('db', 'migrate')
75
+
76
+ # Create a migrator instance.
77
+ migrator = Cassanity::Migrator.new(keyspace, migrations_path)
78
+
79
+ # Run all the pending migrations, in this instance create_users.
80
+ migrator.migrate
81
+ ```
82
+
83
+ You can run `migrator.migrate` over and over and it will only run migrations that have not yet been performed.
84
+
85
+ In addition to `migrate`, you can also `migrate_to` a specific version in a direction.
86
+
87
+ ```ruby
88
+ # assuming the same setup as the above example
89
+
90
+ # migrate all the way back to nothing
91
+ migrator.migrate_to(0, :down)
92
+
93
+ # migrate our users migration again
94
+ migrator.migrate_to(1, :up)
95
+ ```
96
+
97
+ **Note**: When migrating up, the version you declared is included. However, when migrating down, it does not migrate down the version provided.
98
+
99
+ ```ruby
100
+ # run all migrations that have not been performed
101
+ migrator.migrate
102
+
103
+ # invoke the down method of all performed migrations with version > 1
104
+ # note that down will not be invoked for migrations with version of 1
105
+ migrator.migrate_to(1, :down)
106
+
107
+ # invoke the down method for all performed migrations
108
+ migrator.migrate_to(0, :down)
109
+
110
+ # invoke the up method for migrations with version <= 1
111
+ # note that up will be invoked for migrations with version of 1
112
+ migrator.migrate_to(1, :up)
113
+ ```
114
+
115
+ ## Migration Version Numbers
116
+
117
+ In the example above, I used a migration with a version of 001. The only requirement cassanity enforces on migration version numbers is that they be an integer. This means that you can you timestamped migrations if you prefer. For example, the following are all completely valid migration names.
118
+
119
+ ```
120
+ touch db/migrate/001_create_users.rb
121
+ touch db/migrate/201303_create_users.rb
122
+ touch db/migrate/20130304_create_users.rb
123
+ touch db/migrate/20130304160000_create_users.rb
124
+ ```
125
+
126
+ The version could be year, month, day, hour, minute, second (20130304160000, 4:00pm on March 4, 2013), as in the example I just showed, or you could standardize on year, month, day (20130304, March 4, 2013).
127
+
128
+ **Note**: Migrations will run in the order they appear on disk. They are sorted by their path, which means if you do not go with timestamped migrations, you should padd migrations with leading zero's, as Rails does (ie: "001" instead of "01" or "1").
129
+
130
+ ## Rake Tasks
131
+
132
+ What is that? You want some rake tasks to handle migrating? [Here is a gist for that](https://gist.github.com/jnunemaker/5086063). I will try to keep it up to date and I am definitely open to any solutions that would make integrating all of this with your app easier.
@@ -30,18 +30,20 @@ keyspace.create
30
30
  # use this keyspace
31
31
  keyspace.use
32
32
 
33
+ apps_schema = {
34
+ primary_key: :id,
35
+ columns: {
36
+ id: :text,
37
+ name: :text,
38
+ },
39
+ }
40
+
33
41
  # get an instance of a column family
34
42
  apps = keyspace.column_family('apps')
35
43
 
36
44
  # you can also pass a schema so the column family is all knowing
37
45
  apps = keyspace.column_family('apps', {
38
- schema: {
39
- primary_key: :id,
40
- columns: {
41
- id: :text,
42
- name: :text,
43
- },
44
- },
46
+ schema: apps_schema,
45
47
  })
46
48
  pp apps
47
49
 
@@ -52,3 +54,5 @@ apps = Cassanity::ColumnFamily.new({
52
54
  schema: apps_schema,
53
55
  })
54
56
  pp apps
57
+
58
+ keyspace.recreate
@@ -15,7 +15,7 @@ module Cassanity
15
15
  def call(args = {})
16
16
  name = args.fetch(:column_family_name)
17
17
  where = args.fetch(:where)
18
- columns = args.fetch(:columns) { [] }
18
+ columns = Array(args.fetch(:columns) { [] })
19
19
  using = args[:using]
20
20
 
21
21
  if (keyspace_name = args[:keyspace_name])
@@ -0,0 +1,33 @@
1
+ require 'cassanity/argument_generators/where_clause'
2
+
3
+ module Cassanity
4
+ module ArgumentGenerators
5
+ class Columns
6
+
7
+ def initialize(args = {})
8
+ @where_clause = args.fetch(:where_clause) { WhereClause.new }
9
+ end
10
+
11
+ # Internal
12
+ def call(args = {})
13
+ where = {}
14
+ variables = []
15
+ cql = 'SELECT * FROM system.schema_columns'
16
+
17
+ if (keyspace_name = args[:keyspace_name])
18
+ where[:keyspace] = keyspace_name
19
+ end
20
+
21
+ if (column_family_name = args[:column_family_name])
22
+ where[:columnfamily] = column_family_name
23
+ end
24
+
25
+ where_cql, *where_variables = @where_clause.call(where: where)
26
+ cql << where_cql
27
+ variables.concat(where_variables)
28
+
29
+ [cql, *variables]
30
+ end
31
+ end
32
+ end
33
+ end
@@ -34,7 +34,7 @@ module Cassanity
34
34
  wheres << "#{key} #{value.symbol} ?"
35
35
  variables << value.value
36
36
  else
37
- wheres << "#{key} = ?"
37
+ wheres << "\"#{key}\" = ?"
38
38
  variables << value
39
39
  end
40
40
  end
@@ -29,12 +29,14 @@ module Cassanity
29
29
  @options = options.merge(cql_version: '3.0.0')
30
30
  @thrift_options = thrift_options.dup
31
31
  @instrumenter = @options.delete(:instrumenter)
32
+ @retry_strategy = @options.delete(:retry_strategy)
32
33
 
33
34
  @driver = CassandraCQL::Database.new(@servers, @options, @thrift_options)
34
35
 
35
36
  @executor = Cassanity::Executors::CassandraCql.new({
36
- client: @driver,
37
+ driver: @driver,
37
38
  instrumenter: @instrumenter,
39
+ retry_strategy: @retry_strategy,
38
40
  })
39
41
 
40
42
  @connection = Cassanity::Connection.new({
@@ -0,0 +1,48 @@
1
+ module Cassanity
2
+ class Column
3
+
4
+ Types = {
5
+ "org.apache.cassandra.db.marshal.AsciiType" => :ascii,
6
+ "org.apache.cassandra.db.marshal.BooleanType" => :boolean,
7
+ "org.apache.cassandra.db.marshal.BytesType" => :blob,
8
+ "org.apache.cassandra.db.marshal.CounterColumnType" => :counter,
9
+ "org.apache.cassandra.db.marshal.DateType" => :timestamp,
10
+ "org.apache.cassandra.db.marshal.DecimalType" => :decimal,
11
+ "org.apache.cassandra.db.marshal.DoubleType" => :double,
12
+ "org.apache.cassandra.db.marshal.FloatType" => :float,
13
+ "org.apache.cassandra.db.marshal.Int32Type" => :int,
14
+ "org.apache.cassandra.db.marshal.InetAddressType" => :inet,
15
+ "org.apache.cassandra.db.marshal.IntegerType" => :varint,
16
+ "org.apache.cassandra.db.marshal.LongType" => :bigint,
17
+ "org.apache.cassandra.db.marshal.TimeUUIDType" => :timeuuid,
18
+ "org.apache.cassandra.db.marshal.UTF8Type" => :text,
19
+ "org.apache.cassandra.db.marshal.UUIDType" => :uuid,
20
+ }
21
+
22
+ # Public: The name of the column.
23
+ attr_reader :name
24
+
25
+ # Public: The type of the column.
26
+ attr_reader :type
27
+
28
+ # Public: The Cassanity::ColumnFamily the column is in.
29
+ attr_reader :column_family
30
+
31
+ def initialize(args = {})
32
+ @name = args.fetch(:name).to_sym
33
+ type = args.fetch(:type)
34
+ @type = Types.fetch(type, type)
35
+ @column_family = args.fetch(:column_family)
36
+ end
37
+
38
+ # Public
39
+ def inspect
40
+ attributes = [
41
+ "name=#{@name.inspect}",
42
+ "type=#{@type.inspect}",
43
+ "column_family=#{@column_family.inspect}",
44
+ ]
45
+ "#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
46
+ end
47
+ end
48
+ end
@@ -24,7 +24,7 @@ module Cassanity
24
24
  # :schema - The schema used to create the column family (optional).
25
25
  #
26
26
  def initialize(args = {})
27
- @name = args.fetch(:name)
27
+ @name = args.fetch(:name).to_sym
28
28
  @keyspace = args.fetch(:keyspace)
29
29
  @executor = args.fetch(:executor) { @keyspace.executor }
30
30
 
@@ -45,8 +45,11 @@ module Cassanity
45
45
  command: :column_families,
46
46
  arguments: {
47
47
  keyspace_name: @keyspace.name,
48
+ },
49
+ transformer_arguments: {
50
+ keyspace: @keyspace,
48
51
  }
49
- }).any? { |row| row['columnfamily'].to_s == @name.to_s }
52
+ }).any? { |column_family| column_family.name == @name }
50
53
  end
51
54
 
52
55
  alias_method :exist?, :exists?
@@ -290,6 +293,22 @@ module Cassanity
290
293
  })
291
294
  end
292
295
 
296
+ # Public: Get all columns for column family.
297
+ #
298
+ # Returns Array of Cassanity::Column instances.
299
+ def columns
300
+ @executor.call({
301
+ command: :columns,
302
+ arguments: {
303
+ keyspace_name: @keyspace.name,
304
+ column_family_name: @name,
305
+ },
306
+ transformer_arguments: {
307
+ column_family: self,
308
+ }
309
+ })
310
+ end
311
+
293
312
  # Internal
294
313
  def schema
295
314
  @schema || raise(Cassanity::Error.new(message: "No schema found to create #{@name} column family. Please set :schema during initialization or include it as a key in #create call."))