cassanity 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -1
- data/.travis.yml +1 -0
- data/Gemfile +3 -0
- data/Guardfile +0 -2
- data/README.md +11 -0
- data/doc/Instrumentation.md +40 -0
- data/doc/Migrations.md +132 -0
- data/examples/keyspaces.rb +11 -7
- data/lib/cassanity/argument_generators/column_family_delete.rb +1 -1
- data/lib/cassanity/argument_generators/columns.rb +33 -0
- data/lib/cassanity/argument_generators/where_clause.rb +1 -1
- data/lib/cassanity/client.rb +3 -1
- data/lib/cassanity/column.rb +48 -0
- data/lib/cassanity/column_family.rb +21 -2
- data/lib/cassanity/connection.rb +4 -8
- data/lib/cassanity/error.rb +18 -11
- data/lib/cassanity/executors/cassandra_cql.rb +79 -50
- data/lib/cassanity/instrumentation/log_subscriber.rb +4 -5
- data/lib/cassanity/instrumentation/metriks.rb +6 -0
- data/lib/cassanity/instrumentation/metriks_subscriber.rb +16 -0
- data/lib/cassanity/instrumentation/statsd.rb +6 -0
- data/lib/cassanity/instrumentation/statsd_subscriber.rb +22 -0
- data/lib/cassanity/instrumentation/subscriber.rb +58 -0
- data/lib/cassanity/instrumenters/memory.rb +0 -1
- data/lib/cassanity/keyspace.rb +10 -8
- data/lib/cassanity/migration.rb +125 -0
- data/lib/cassanity/migration_proxy.rb +76 -0
- data/lib/cassanity/migrator.rb +154 -0
- data/lib/cassanity/result_transformers/column_families.rb +20 -0
- data/lib/cassanity/result_transformers/columns.rb +21 -0
- data/lib/cassanity/result_transformers/keyspaces.rb +21 -0
- data/lib/cassanity/result_transformers/mirror.rb +1 -1
- data/lib/cassanity/result_transformers/result_to_array.rb +1 -1
- data/lib/cassanity/retry_strategies/exponential_backoff.rb +43 -0
- data/lib/cassanity/retry_strategies/retry_n_times.rb +29 -0
- data/lib/cassanity/retry_strategies/retry_strategy.rb +35 -0
- data/lib/cassanity/version.rb +1 -1
- data/spec/helper.rb +8 -0
- data/spec/integration/cassanity/column_family_spec.rb +36 -25
- data/spec/integration/cassanity/connection_spec.rb +11 -11
- data/spec/integration/cassanity/fixtures/migrations/20130224135000_create_users.rb +17 -0
- data/spec/integration/cassanity/fixtures/migrations/20130225135002_create_apps.rb +15 -0
- data/spec/integration/cassanity/fixtures/migrations/20130226135004_add_username_to_users.rb +9 -0
- data/spec/integration/cassanity/instrumentation/log_subscriber_spec.rb +71 -0
- data/spec/integration/cassanity/instrumentation/metriks_subscriber_spec.rb +48 -0
- data/spec/integration/cassanity/instrumentation/statsd_subscriber_spec.rb +58 -0
- data/spec/integration/cassanity/keyspace_spec.rb +21 -21
- data/spec/integration/cassanity/migration_spec.rb +157 -0
- data/spec/integration/cassanity/migrator_spec.rb +212 -0
- data/spec/support/cassanity_helpers.rb +21 -17
- data/spec/support/fake_udp_socket.rb +27 -0
- data/spec/unit/cassanity/argument_generators/batch_spec.rb +5 -5
- data/spec/unit/cassanity/argument_generators/column_family_delete_spec.rb +20 -6
- data/spec/unit/cassanity/argument_generators/column_family_update_spec.rb +6 -6
- data/spec/unit/cassanity/argument_generators/columns_spec.rb +45 -0
- data/spec/unit/cassanity/argument_generators/keyspace_create_spec.rb +1 -1
- data/spec/unit/cassanity/argument_generators/keyspace_drop_spec.rb +1 -1
- data/spec/unit/cassanity/argument_generators/keyspace_use_spec.rb +1 -1
- data/spec/unit/cassanity/argument_generators/where_clause_spec.rb +2 -2
- data/spec/unit/cassanity/client_spec.rb +10 -3
- data/spec/unit/cassanity/column_family_spec.rb +20 -3
- data/spec/unit/cassanity/column_spec.rb +76 -0
- data/spec/unit/cassanity/connection_spec.rb +1 -1
- data/spec/unit/cassanity/error_spec.rb +7 -2
- data/spec/unit/cassanity/executors/cassandra_cql_spec.rb +76 -23
- data/spec/unit/cassanity/keyspace_spec.rb +38 -13
- data/spec/unit/cassanity/migration_proxy_spec.rb +81 -0
- data/spec/unit/cassanity/migration_spec.rb +12 -0
- data/spec/unit/cassanity/migrator_spec.rb +20 -0
- data/spec/unit/cassanity/retry_strategies/exponential_backoff_spec.rb +37 -0
- data/spec/unit/cassanity/retry_strategies/retry_n_times_spec.rb +47 -0
- data/spec/unit/cassanity/retry_strategies/retry_strategy_spec.rb +27 -0
- metadata +56 -4
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/Guardfile
CHANGED
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.
|
data/examples/keyspaces.rb
CHANGED
@@ -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
|
data/lib/cassanity/client.rb
CHANGED
@@ -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
|
-
|
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? { |
|
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."))
|