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
@@ -42,16 +42,12 @@ module Cassanity
42
42
  #
43
43
  # Returns Array of Cassanity::Keyspace instances.
44
44
  def keyspaces
45
- rows = @executor.call({
45
+ @executor.call({
46
46
  command: :keyspaces,
47
- })
48
-
49
- rows.map { |row|
50
- Keyspace.new({
51
- name: row['name'],
47
+ transformer_arguments: {
52
48
  executor: @executor,
53
- })
54
- }
49
+ },
50
+ })
55
51
  end
56
52
 
57
53
  # Public: Get a keyspace instance
@@ -1,5 +1,5 @@
1
1
  module Cassanity
2
- class Error < Exception
2
+ class Error < StandardError
3
3
  # Public: The original error this exception is wrapping.
4
4
  attr_reader :original
5
5
 
@@ -10,19 +10,26 @@ module Cassanity
10
10
  #
11
11
  # Returns the duplicated String.
12
12
  def initialize(args = {})
13
- @original = args.fetch(:original) { $! }
14
- @message = args.fetch(:message) {
15
- if @original
16
- "Original Exception: #{@original.class}: #{@original.message}"
17
- else
18
- "Something truly horrible went wrong"
19
- end
20
- }
13
+ if args.is_a?(String)
14
+ @message = args
15
+ else
16
+ @original = args.fetch(:original) { $! }
17
+ @message = args.fetch(:message) {
18
+ if @original
19
+ "Original Exception: #{@original.class}: #{@original.message}"
20
+ else
21
+ "Something truly horrible went wrong"
22
+ end
23
+ }
24
+ end
21
25
 
22
26
  super @message
23
27
  end
24
28
  end
25
29
 
26
- class UnknownCommand < Error
27
- end
30
+ # Raised when an argument generator is asked to perform an unknown command.
31
+ UnknownCommand = Class.new(Error)
32
+
33
+ # Raised when a migration operation is attempted that is not supported.
34
+ MigrationOperationNotSupported = Class.new(Error)
28
35
  end
@@ -18,8 +18,14 @@ require 'cassanity/argument_generators/column_family_alter'
18
18
  require 'cassanity/argument_generators/index_create'
19
19
  require 'cassanity/argument_generators/index_drop'
20
20
  require 'cassanity/argument_generators/batch'
21
+ require 'cassanity/argument_generators/columns'
21
22
  require 'cassanity/result_transformers/result_to_array'
23
+ require 'cassanity/result_transformers/keyspaces'
24
+ require 'cassanity/result_transformers/column_families'
25
+ require 'cassanity/result_transformers/columns'
22
26
  require 'cassanity/result_transformers/mirror'
27
+ require 'cassanity/retry_strategies/retry_n_times'
28
+ require 'cassanity/retry_strategies/exponential_backoff'
23
29
 
24
30
  module Cassanity
25
31
  module Executors
@@ -27,40 +33,45 @@ module Cassanity
27
33
  extend Forwardable
28
34
 
29
35
  # Private: Hash of commands to related argument generators.
30
- ArgumentGenerators = {
31
- keyspaces: Cassanity::ArgumentGenerators::Keyspaces.new,
32
- keyspace_create: Cassanity::ArgumentGenerators::KeyspaceCreate.new,
33
- keyspace_drop: Cassanity::ArgumentGenerators::KeyspaceDrop.new,
34
- keyspace_use: Cassanity::ArgumentGenerators::KeyspaceUse.new,
35
- column_families: Cassanity::ArgumentGenerators::ColumnFamilies.new,
36
- column_family_create: Cassanity::ArgumentGenerators::ColumnFamilyCreate.new,
37
- column_family_drop: Cassanity::ArgumentGenerators::ColumnFamilyDrop.new,
38
- column_family_truncate: Cassanity::ArgumentGenerators::ColumnFamilyTruncate.new,
39
- column_family_select: Cassanity::ArgumentGenerators::ColumnFamilySelect.new,
40
- column_family_insert: Cassanity::ArgumentGenerators::ColumnFamilyInsert.new,
41
- column_family_update: Cassanity::ArgumentGenerators::ColumnFamilyUpdate.new,
42
- column_family_delete: Cassanity::ArgumentGenerators::ColumnFamilyDelete.new,
43
- column_family_alter: Cassanity::ArgumentGenerators::ColumnFamilyAlter.new,
44
- index_create: Cassanity::ArgumentGenerators::IndexCreate.new,
45
- index_drop: Cassanity::ArgumentGenerators::IndexDrop.new,
46
- batch: Cassanity::ArgumentGenerators::Batch.new,
36
+ DefaultArgumentGenerators = {
37
+ keyspaces: ArgumentGenerators::Keyspaces.new,
38
+ keyspace_create: ArgumentGenerators::KeyspaceCreate.new,
39
+ keyspace_drop: ArgumentGenerators::KeyspaceDrop.new,
40
+ keyspace_use: ArgumentGenerators::KeyspaceUse.new,
41
+ column_families: ArgumentGenerators::ColumnFamilies.new,
42
+ column_family_create: ArgumentGenerators::ColumnFamilyCreate.new,
43
+ column_family_drop: ArgumentGenerators::ColumnFamilyDrop.new,
44
+ column_family_truncate: ArgumentGenerators::ColumnFamilyTruncate.new,
45
+ column_family_select: ArgumentGenerators::ColumnFamilySelect.new,
46
+ column_family_insert: ArgumentGenerators::ColumnFamilyInsert.new,
47
+ column_family_update: ArgumentGenerators::ColumnFamilyUpdate.new,
48
+ column_family_delete: ArgumentGenerators::ColumnFamilyDelete.new,
49
+ column_family_alter: ArgumentGenerators::ColumnFamilyAlter.new,
50
+ index_create: ArgumentGenerators::IndexCreate.new,
51
+ index_drop: ArgumentGenerators::IndexDrop.new,
52
+ batch: ArgumentGenerators::Batch.new,
53
+ columns: ArgumentGenerators::Columns.new,
47
54
  }
48
55
 
49
56
  # Private: Hash of commands to related result transformers.
50
- ResultTransformers = {
51
- keyspaces: Cassanity::ResultTransformers::ResultToArray.new,
52
- column_families: Cassanity::ResultTransformers::ResultToArray.new,
53
- column_family_select: Cassanity::ResultTransformers::ResultToArray.new,
57
+ DefaultResultTransformers = {
58
+ keyspaces: ResultTransformers::Keyspaces.new,
59
+ column_families: ResultTransformers::ColumnFamilies.new,
60
+ column_family_select: ResultTransformers::ResultToArray.new,
61
+ columns: ResultTransformers::Columns.new,
54
62
  }
55
63
 
64
+ # Private: Default retry strategy to retry N times.
65
+ DefaultRetryStrategy = RetryStrategies::RetryNTimes.new
66
+
56
67
  # Private: Default result transformer for commands that do not have one.
57
- Mirror = Cassanity::ResultTransformers::Mirror.new
68
+ Mirror = ResultTransformers::Mirror.new
58
69
 
59
70
  # Private: Forward #instrument to instrumenter.
60
71
  def_delegator :@instrumenter, :instrument
61
72
 
62
73
  # Private
63
- attr_reader :client
74
+ attr_reader :driver
64
75
 
65
76
  # Private
66
77
  attr_reader :argument_generators
@@ -71,10 +82,13 @@ module Cassanity
71
82
  # Private: What should be used to instrument all the things.
72
83
  attr_reader :instrumenter
73
84
 
85
+ # Private: What strategy to use when retrying Cassandra commands
86
+ attr_reader :retry_strategy
87
+
74
88
  # Internal: Initializes a cassandra-cql based CQL executor.
75
89
  #
76
90
  # args - The Hash of arguments.
77
- # :client - The CassandraCQL::Database connection instance.
91
+ # :driver - The CassandraCQL::Database connection instance.
78
92
  # :instrumenter - What should be used to instrument all the things
79
93
  # (default: Cassanity::Instrumenters::Noop).
80
94
  # :argument_generators - A Hash where each key is a command name
@@ -85,17 +99,21 @@ module Cassanity
85
99
  # and each value is the related result
86
100
  # transformer that responds to `call`
87
101
  # (optional).
102
+ # :retry_strategy - What retry strategy to use on failed
103
+ # CassandraCQL calls
104
+ # (default: Cassanity::Instrumenters::RetryNTimes)
88
105
  #
89
106
  # Examples
90
107
  #
91
- # client = CassandraCQL::Database.new('host', cql_version: '3.0.0')
92
- # Cassanity::Executors::CassandraCql.new(client: client)
108
+ # driver = CassandraCQL::Database.new('host', cql_version: '3.0.0')
109
+ # Cassanity::Executors::CassandraCql.new(driver: driver)
93
110
  #
94
111
  def initialize(args = {})
95
- @client = args.fetch(:client)
112
+ @driver = args.fetch(:driver)
96
113
  @instrumenter = args[:instrumenter] || Instrumenters::Noop
97
- @argument_generators = args.fetch(:argument_generators) { ArgumentGenerators }
98
- @result_transformers = args.fetch(:result_transformers) { ResultTransformers }
114
+ @argument_generators = args.fetch(:argument_generators, DefaultArgumentGenerators)
115
+ @result_transformers = args.fetch(:result_transformers, DefaultResultTransformers)
116
+ @retry_strategy = args[:retry_strategy] || DefaultRetryStrategy
99
117
  end
100
118
 
101
119
  # Internal: Execute a CQL query.
@@ -112,42 +130,53 @@ module Cassanity
112
130
  #
113
131
  # call({
114
132
  # command: :keyspace_create,
115
- # arguments: {name: 'analytics'},
133
+ # arguments: {keyspace_name: 'analytics'},
116
134
  # })
117
135
  #
118
136
  # Returns the result of execution.
119
137
  # Raises Cassanity::Error if anything goes wrong during execution.
120
138
  def call(args = {})
121
- instrument('cql.cassanity', {}) do |payload|
122
- command = args.fetch(:command)
123
- generator = @argument_generators.fetch(command)
124
- arguments = args[:arguments]
125
- execute_arguments = generator.call(arguments)
139
+ instrument('cql.cassanity') do |payload|
140
+ begin
141
+ command = args.fetch(:command)
142
+ payload[:command] = command
143
+ generator = @argument_generators.fetch(command)
144
+ rescue KeyError => e
145
+ raise Cassanity::UnknownCommand
146
+ end
126
147
 
127
- result = @client.execute(*execute_arguments)
128
-
129
- transformer = @result_transformers.fetch(command) { Mirror }
130
- transformed_result = transformer.call(result)
148
+ arguments = args[:arguments]
131
149
 
132
- payload[:command] = command
133
- payload[:generator] = generator
134
- payload[:arguments] = arguments
135
- payload[:execute_arguments] = execute_arguments
136
- payload[:result] = result
137
- payload[:transformer] = transformer
150
+ if arguments
151
+ if (keyspace_name = arguments[:keyspace_name])
152
+ payload[:keyspace_name] = keyspace_name
153
+ end
154
+
155
+ if (column_family_name = arguments[:column_family_name])
156
+ payload[:column_family_name] = column_family_name
157
+ end
158
+ end
159
+
160
+ begin
161
+ execute_arguments = generator.call(arguments)
162
+ payload[:cql] = execute_arguments[0]
163
+ payload[:cql_variables] = execute_arguments[1..-1]
164
+ result = @retry_strategy.execute(payload) { @driver.execute(*execute_arguments) }
165
+ transformer = @result_transformers.fetch(command, Mirror)
166
+ transformed_result = transformer.call(result, args[:transformer_arguments])
167
+ payload[:result] = transformed_result
168
+ rescue StandardError => e
169
+ raise Cassanity::Error
170
+ end
138
171
 
139
172
  transformed_result
140
173
  end
141
- rescue KeyError
142
- raise Cassanity::UnknownCommand
143
- rescue Exception => e
144
- raise Cassanity::Error
145
174
  end
146
175
 
147
176
  # Public
148
177
  def inspect
149
178
  attributes = [
150
- "client=#{@client.inspect}",
179
+ "driver=#{@driver.inspect}",
151
180
  ]
152
181
  "#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
153
182
  end
@@ -10,13 +10,12 @@ module Cassanity
10
10
 
11
11
  name = '%s (%.1fms)' % ["CQL Query", event.duration]
12
12
 
13
- # execute arguments are always an array where the first element is the
14
- # cql string and the rest are the bound variables.
15
- cql, *args = event.payload[:execute_arguments]
16
- arguments = args.map { |arg| arg.inspect }.join(', ')
13
+ cql = event.payload[:cql]
14
+ vars = event.payload[:cql_variables] || []
15
+ variables = vars.map { |var| var.inspect }.join(', ')
17
16
 
18
17
  query = "#{cql}"
19
- query += " (#{arguments})" unless arguments.empty?
18
+ query += " (#{variables})" unless variables.empty?
20
19
 
21
20
  debug " #{color(name, CYAN, true)} [ #{query} ]"
22
21
  end
@@ -0,0 +1,6 @@
1
+ require 'securerandom'
2
+ require 'active_support/notifications'
3
+ require 'cassanity/instrumentation/metriks_subscriber'
4
+
5
+ ActiveSupport::Notifications.subscribe 'cql.cassanity',
6
+ Cassanity::Instrumentation::MetriksSubscriber
@@ -0,0 +1,16 @@
1
+ # Note: You should never need to require this file directly if you are using
2
+ # ActiveSupport::Notifications. Instead, you should require the metriks file
3
+ # that lives in the same directory as this file. The benefit is that it
4
+ # subscribes to the correct events and does everything for your.
5
+ require 'cassanity/instrumentation/subscriber'
6
+ require 'metriks'
7
+
8
+ module Cassanity
9
+ module Instrumentation
10
+ class MetriksSubscriber < Subscriber
11
+ def update_timer(metric)
12
+ Metriks.timer(metric).update(@duration)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ require 'securerandom'
2
+ require 'active_support/notifications'
3
+ require 'cassanity/instrumentation/statsd_subscriber'
4
+
5
+ ActiveSupport::Notifications.subscribe 'cql.cassanity',
6
+ Cassanity::Instrumentation::StatsdSubscriber
@@ -0,0 +1,22 @@
1
+ # Note: You should never need to require this file directly if you are using
2
+ # ActiveSupport::Notifications. Instead, you should require the metriks file
3
+ # that lives in the same directory as this file. The benefit is that it
4
+ # subscribes to the correct events and does everything for your.
5
+ require 'cassanity/instrumentation/subscriber'
6
+ require 'statsd'
7
+
8
+ module Cassanity
9
+ module Instrumentation
10
+ class StatsdSubscriber < Subscriber
11
+ class << self
12
+ attr_accessor :client
13
+ end
14
+
15
+ def update_timer(metric)
16
+ if self.class.client
17
+ self.class.client.timing metric, (@duration * 1_000).round
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,58 @@
1
+ module Cassanity
2
+ module Instrumentation
3
+ class Subscriber
4
+ # Public: Use this as the subscribed block.
5
+ def self.call(name, start, ending, transaction_id, payload)
6
+ new(name, start, ending, transaction_id, payload).update
7
+ end
8
+
9
+ # Private: Initializes a new event processing instance.
10
+ def initialize(name, start, ending, transaction_id, payload)
11
+ @name = name
12
+ @start = start
13
+ @ending = ending
14
+ @payload = payload
15
+ @duration = ending - start
16
+ @transaction_id = transaction_id
17
+
18
+ @command_name = @payload[:command]
19
+ @keyspace_name = @payload[:keyspace_name]
20
+ @column_family_name = @payload[:column_family_name]
21
+ end
22
+
23
+ # Public: Actually update all the metriks timers for the event.
24
+ #
25
+ # Returns nothing.
26
+ def update
27
+ update_timer 'cassanity.cql'
28
+
29
+ if command_name?
30
+ update_timer "cassanity.command.#{@command_name}.cql"
31
+ end
32
+
33
+ if column_family_name?
34
+ update_timer "cassanity.column_family.#{@column_family_name}.cql"
35
+ end
36
+
37
+ if column_family_name? && command_name?
38
+ update_timer "cassanity.column_family.#{@column_family_name}.#{@command_name}.cql"
39
+ end
40
+ end
41
+
42
+ # Internal: Override in subclass.
43
+ def update_timer(metric)
44
+ raise 'not implemented'
45
+ end
46
+
47
+ # Private: Returns true if command name present else false.
48
+ def command_name?
49
+ @command_name_present ||= !@command_name.nil? && !@command_name.empty?
50
+ end
51
+
52
+ # Private: Returns true if column family name present else false.
53
+ def column_family_name?
54
+ @column_family_name_present ||= !@column_family_name.nil? && !@column_family_name.empty?
55
+ end
56
+ end
57
+ end
58
+ end
@@ -19,7 +19,6 @@ module Cassanity
19
19
  end
20
20
 
21
21
  @events << Event.new(name, payload, result)
22
-
23
22
  result
24
23
  end
25
24
  end
@@ -25,7 +25,7 @@ module Cassanity
25
25
  # creating keyspace.
26
26
  #
27
27
  def initialize(args = {})
28
- @name = args.fetch(:name)
28
+ @name = args.fetch(:name).to_sym
29
29
  @executor = args.fetch(:executor)
30
30
  @strategy_class = args[:strategy_class]
31
31
  @strategy_options = args[:strategy_options]
@@ -36,8 +36,12 @@ module Cassanity
36
36
  #
37
37
  # Returns true if keyspace exists, false if it does not.
38
38
  def exists?
39
- rows = @executor.call(command: :keyspaces)
40
- rows.any? { |row| row['name'].to_s == name.to_s }
39
+ @executor.call({
40
+ command: :keyspaces,
41
+ transformer_arguments: {
42
+ executor: @executor,
43
+ },
44
+ }).any? { |keyspace| keyspace.name == @name }
41
45
  end
42
46
 
43
47
  alias_method :exist?, :exists?
@@ -125,12 +129,10 @@ module Cassanity
125
129
  arguments: {
126
130
  keyspace_name: @name,
127
131
  },
128
- }).map { |row|
129
- ColumnFamily.new({
130
- name: row['columnfamily'],
132
+ transformer_arguments: {
131
133
  keyspace: self,
132
- })
133
- }
134
+ }
135
+ })
134
136
  end
135
137
 
136
138
  # Public: Get a column family instance