cassanity 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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