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
@@ -0,0 +1,125 @@
1
+ require 'forwardable'
2
+
3
+ module Cassanity
4
+ class Migration
5
+ extend Forwardable
6
+
7
+ # Private: The migrator that is running the migration.
8
+ attr_reader :migrator
9
+
10
+ # Private: Delegates keyspace to migrator.
11
+ def_delegator :@migrator, :keyspace
12
+
13
+ # Public: Get new instance of a migration.
14
+ #
15
+ # migrator - The Cassanity::Migrator instance that is running the show.
16
+ def initialize(migrator)
17
+ @migrator = migrator
18
+ end
19
+
20
+ # Public: Override in subclass.
21
+ def up
22
+ end
23
+
24
+ # Public: Override in subclass.
25
+ def down
26
+ end
27
+
28
+ # Public: Create a column family.
29
+ #
30
+ # column_family_name - The String or Symbol name of the column family.
31
+ # args - The Hash of arguments. See ColumnFamily#create for available args.
32
+ #
33
+ # Returns nothing.
34
+ def create_column_family(column_family_name, schema)
35
+ keyspace.column_family(column_family_name, schema: schema).create
36
+ end
37
+ alias_method :add_column_family, :create_column_family
38
+ alias_method :create_table, :create_column_family
39
+ alias_method :add_table, :create_column_family
40
+
41
+ # Public: Drop a column family.
42
+ #
43
+ # column_family_name - The String or Symbol name of the column family.
44
+ #
45
+ # Returns nothing.
46
+ def drop_column_family(column_family_name)
47
+ keyspace[column_family_name].drop
48
+ end
49
+ alias_method :drop_table, :drop_column_family
50
+
51
+ # Public: Add a column to a column family.
52
+ #
53
+ # column_family_name - The String or Symbol name of the column family.
54
+ # column_name - The String or Symbol name of the column to index.
55
+ # type - The String or Symbol CQL data type for the column.
56
+ #
57
+ # Returns nothing.
58
+ def add_column(column_family_name, column_name, type)
59
+ keyspace[column_family_name].alter(add: {column_name => type})
60
+ end
61
+ alias_method :create_column, :add_column
62
+
63
+ # Public: Drop a column from a column family.
64
+ #
65
+ # column_family_name - The String or Symbol name of the column family.
66
+ # column_name - The String or Symbol name of the column to index.
67
+ #
68
+ # Returns nothing.
69
+ def drop_column(column_family_name, column_name)
70
+ keyspace[column_family_name].alter(drop: column_name)
71
+ end
72
+
73
+ # Public: Alter a column family.
74
+ #
75
+ # column_family_name - The String or Symbol name of the column family.
76
+ # args - The Hash of arguments. See ColumnFamily#alter for available args.
77
+ #
78
+ # Returns nothing.
79
+ def alter_column_family(column_family_name, args = {})
80
+ keyspace[column_family_name].alter(args)
81
+ end
82
+ alias_method :alter_table, :alter_column_family
83
+
84
+ # Public: Create an index on a column for a column family.
85
+ #
86
+ # column_family_name - The String or Symbol name of the column family.
87
+ # column_name - The String or Symbol name of the column to index.
88
+ # options - The Hash of options.
89
+ # :name - The String or Symbol name of the index
90
+ # (defaults to column_name).
91
+ #
92
+ # Returns nothing.
93
+ def add_index(column_family_name, column_name, options = {})
94
+ index_args = {column_name: column_name}
95
+ index_args[:name] = options[:name] if options.key?(:name)
96
+
97
+ keyspace[column_family_name].create_index(index_args)
98
+ end
99
+ alias_method :create_index, :add_index
100
+
101
+ # Public: Drop an index by name for a column family.
102
+ #
103
+ # column_family_name - The String or Symbol name of the column family.
104
+ # name - The String or Symbol name of the index.
105
+ #
106
+ # Returns nothing.
107
+ def drop_index(column_family_name, index_name)
108
+ keyspace[column_family_name].drop_index(name: index_name)
109
+ end
110
+
111
+ # Public: Spit something to the log with timing.
112
+ def say_with_time(message)
113
+ say message
114
+ start = Time.now
115
+ result = yield
116
+ duration = (Time.now - start).round(3)
117
+ @migrator.log " -> #{duration}s"
118
+ end
119
+
120
+ # Public: Spit something to the log.
121
+ def say(message)
122
+ @migrator.log "-- #{message}"
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,76 @@
1
+ require 'pathname'
2
+
3
+ module Cassanity
4
+ class MigrationProxy
5
+ include Comparable
6
+
7
+ # Public: The full path to the migration on disk.
8
+ attr_reader :path
9
+
10
+ # Public: The version of the migration.
11
+ attr_reader :version
12
+
13
+ # Public: The name of the migration.
14
+ attr_reader :name
15
+
16
+ def initialize(path)
17
+ raise ArgumentError, "path cannot be nil" if path.nil?
18
+
19
+ basename = File.basename(path, '.rb')
20
+ version, name = basename.split('_', 2)
21
+
22
+ raise ArgumentError, "version cannot be nil" if version.nil?
23
+ raise ArgumentError, "name cannot be nil" if name.nil?
24
+
25
+ @path = Pathname(path)
26
+ @version = version.to_i
27
+ @name = name
28
+ end
29
+
30
+ def up(migrator)
31
+ log(migrator) { build_migration(migrator).up }
32
+ end
33
+
34
+ def down(migrator)
35
+ log(migrator) { build_migration(migrator).down }
36
+ end
37
+
38
+ def log(migrator)
39
+ migrator.log "== #{@name}: migrating ".ljust(80, "=")
40
+ start = Time.now
41
+ result = yield
42
+ duration = (Time.now - start).round(3)
43
+ migrator.log "== #{@name}: migrated (#{duration}s) ".ljust(80, "=")
44
+ result
45
+ end
46
+
47
+ def build_migration(migrator)
48
+ migration_class.new(migrator)
49
+ end
50
+
51
+ def migration_class
52
+ @migration_class ||= begin
53
+ require path
54
+ # TODO: handle constant not found
55
+ Kernel.const_get(constant)
56
+ end
57
+ end
58
+
59
+ def constant
60
+ name.split('_').map { |word| word.capitalize }.join('')
61
+ end
62
+
63
+ def hash
64
+ path.hash
65
+ end
66
+
67
+ def eql?(other)
68
+ self.class.eql?(other.class) && path == other.path
69
+ end
70
+ alias_method :==, :eql?
71
+
72
+ def <=>(other)
73
+ @path <=> other.path
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,154 @@
1
+ require 'logger'
2
+ require 'pathname'
3
+ require 'cassanity/migration_proxy'
4
+ require 'cassanity/migration'
5
+
6
+ module Cassanity
7
+ class Migrator
8
+ SupportedDirections = [:up, :down]
9
+
10
+ # Public: The keyspace all migrations apply to.
11
+ attr_reader :keyspace
12
+
13
+ # Public: The path to all the migrations.
14
+ attr_reader :migrations_path
15
+
16
+ # Public: Where to spit all the logging related to migrations.
17
+ attr_reader :logger
18
+
19
+ def initialize(keyspace, migrations_path, options = {})
20
+ @keyspace = keyspace
21
+ @migrations_path = Pathname(migrations_path)
22
+ @logger = options[:logger] || default_logger
23
+ end
24
+
25
+ # Public: Runs all pending migrations in order.
26
+ def migrate
27
+ run_migrations pending_migrations, :up
28
+ end
29
+
30
+ # Public: Migrates to a version using a direction.
31
+ #
32
+ # version - The String or Integer version to migrate to.
33
+ # direction - The String or Symbol direction you would like to migrate.
34
+ def migrate_to(version, direction = :up)
35
+ version = version.to_i
36
+ direction = direction.to_sym
37
+ assert_valid_direction(direction)
38
+
39
+ migrations = case direction
40
+ when :up
41
+ pending_migrations.select { |migration| migration.version <= version }
42
+ when :down
43
+ performed_migrations.select { |migration| migration.version > version }
44
+ else
45
+ []
46
+ end
47
+
48
+ run_migrations migrations, direction
49
+ end
50
+
51
+ # Public: Marks a migration as migrated.
52
+ def migrated_up(migration)
53
+ column_family.insert({
54
+ data: {
55
+ version: migration.version,
56
+ name: migration.name,
57
+ migrated_at: Time.now.utc,
58
+ },
59
+ })
60
+ end
61
+
62
+ # Public: Marks a migration as not run.
63
+ def migrated_down(migration)
64
+ column_family.delete({
65
+ where: {
66
+ version: migration.version,
67
+ name: migration.name,
68
+ },
69
+ })
70
+ end
71
+
72
+ # Public: An array of all migrations.
73
+ def migrations
74
+ @migrations ||= begin
75
+ paths = Dir["#{migrations_path}/*.rb"]
76
+ migrations = paths.map { |path| MigrationProxy.new(path) }
77
+ migrations.sort
78
+ end
79
+ end
80
+
81
+ # Public: An array of the migrations that have been performed.
82
+ def performed_migrations
83
+ rows = column_family.select
84
+ rows.map { |row|
85
+ path = migrations_path.join("#{row['version']}_#{row['name']}.rb")
86
+ MigrationProxy.new(path)
87
+ }.sort
88
+ end
89
+
90
+ # Public: An array of the migrations that have not been performed.
91
+ def pending_migrations
92
+ (migrations - performed_migrations).sort
93
+ end
94
+
95
+ # Internal: Log a message.
96
+ def log(message)
97
+ @logger.info message
98
+ end
99
+
100
+ # Private
101
+ def run_migrations(migrations, direction)
102
+ migrations = migrations.sort
103
+
104
+ case direction
105
+ when :up
106
+ migrations.each do |migration|
107
+ migration.up(self)
108
+ migrated_up(migration)
109
+ end
110
+ when :down
111
+ migrations = migrations.reverse
112
+ migrations.each do |migration|
113
+ migration.down(self)
114
+ migrated_down(migration)
115
+ end
116
+ end
117
+
118
+ {performed: migrations}
119
+ end
120
+
121
+ # Private: The column family storing all
122
+ # migration information.
123
+ def column_family
124
+ @column_family ||= begin
125
+ column_family = keyspace.column_family({
126
+ name: :migrations,
127
+ schema: {
128
+ primary_key: [:version, :name],
129
+ columns: {
130
+ version: :text,
131
+ name: :text,
132
+ migrated_at: :timestamp,
133
+ },
134
+ },
135
+ })
136
+ column_family.create unless column_family.exists?
137
+ column_family
138
+ end
139
+ end
140
+
141
+ # Private
142
+ def default_logger
143
+ logger = Logger.new(STDOUT)
144
+ logger.formatter = proc { |_, _, _, msg| "#{msg}\n" }
145
+ logger
146
+ end
147
+
148
+ def assert_valid_direction(direction)
149
+ unless SupportedDirections.include?(direction)
150
+ raise ArgumentError, "#{direction.inspect} is not a valid migration direction"
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,20 @@
1
+ require 'cassanity/column_family'
2
+
3
+ module Cassanity
4
+ module ResultTransformers
5
+ class ColumnFamilies
6
+
7
+ # Internal: Turns result into Array of column families.
8
+ def call(result, args = {})
9
+ column_families = []
10
+ result.fetch_hash do |row|
11
+ column_families << ColumnFamily.new({
12
+ name: row['columnfamily'] || row['columnfamily_name'],
13
+ keyspace: args[:keyspace],
14
+ })
15
+ end
16
+ column_families
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ require 'cassanity/column'
2
+
3
+ module Cassanity
4
+ module ResultTransformers
5
+ class Columns
6
+
7
+ # Internal: Turns result into Array of column families.
8
+ def call(result, args = {})
9
+ columns = []
10
+ result.fetch_hash do |row|
11
+ columns << Column.new({
12
+ name: row['column'],
13
+ type: row['validator'],
14
+ column_family: args[:column_family],
15
+ })
16
+ end
17
+ columns
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ require 'cassanity/column_family'
2
+
3
+ module Cassanity
4
+ module ResultTransformers
5
+ class Keyspaces
6
+
7
+ # Internal: Turns result into Array of keyspaces.
8
+ def call(result, args = {})
9
+ keyspaces = []
10
+ result.fetch_hash do |row|
11
+ name = row['name'] || row['keyspace'] || row['keyspace_name']
12
+ keyspaces << Keyspace.new({
13
+ name: name,
14
+ executor: args[:executor],
15
+ })
16
+ end
17
+ keyspaces
18
+ end
19
+ end
20
+ end
21
+ end
@@ -4,7 +4,7 @@ module Cassanity
4
4
 
5
5
  # Internal: Returns whatever result is passed to it. This is used as the
6
6
  # default result transformer when a command does not have one.
7
- def call(result)
7
+ def call(result, args = nil)
8
8
  result
9
9
  end
10
10
  end
@@ -3,7 +3,7 @@ module Cassanity
3
3
  class ResultToArray
4
4
 
5
5
  # Internal: Turns result into Array of Hashes.
6
- def call(result)
6
+ def call(result, args = nil)
7
7
  rows = []
8
8
  result.fetch_hash do |row|
9
9
  rows << row
@@ -0,0 +1,43 @@
1
+ require 'cassanity/retry_strategies/retry_strategy'
2
+
3
+ module Cassanity
4
+ module RetryStrategies
5
+ class ExponentialBackoff < RetryStrategy
6
+ ForeverSentinel = :forever
7
+
8
+ # Private: Taken from https://github.com/twitter/kestrel-client's
9
+ # blocking client.
10
+ SleepTimes = [[0] * 1, [0.01] * 2, [0.1] * 2, [0.5] * 2, [1.0] * 1].flatten
11
+
12
+ # Private: the maxmimum number of times to retry or -1 to try forever.
13
+ attr_reader :retries
14
+
15
+ # Public: Initialize the retry strategy.
16
+ #
17
+ # args - The Hash of options.
18
+ # :retries - the maximum number of times to retry (default: forever)
19
+ def initialize(args = {})
20
+ # The default behavior is to retry forever.
21
+ @retries = args[:retries] || ForeverSentinel
22
+ end
23
+
24
+ def fail(attempts, error)
25
+ if @retries != ForeverSentinel && attempts > @retries
26
+ raise error
27
+ end
28
+ sleep_for_count(attempts)
29
+ end
30
+
31
+ # Private: sleep a randomized amount of time from the SleepTimes
32
+ # mostly-exponential distribution.
33
+ #
34
+ # count - the index into the distribution to pull the base sleep time from
35
+ def sleep_for_count(count)
36
+ base = SleepTimes[count] || SleepTimes.last
37
+
38
+ time = ((rand * base) + base) / 2
39
+ sleep time if time > 0
40
+ end
41
+ end
42
+ end
43
+ end