cassandra-web 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,15 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 3a6f4cd8f179852f512ed15d970fa047b9bd8e34
4
- data.tar.gz: 72e44f686c76317e73080cba9cff33eca6eddd5d
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MDIxN2VhZDIyOGQwMmQzM2M3ZjUyMjg0ZTMwNDU5MWZhZmM2ODljZg==
5
+ data.tar.gz: !binary |-
6
+ OTAxYjlmNWQ5ZDI2Y2FiZjUyMjdhMGExNzZkYzk4NTEzZTI5N2VhMw==
5
7
  SHA512:
6
- metadata.gz: b5bbf71fef357fd138aa3fe3f968cc3e5f01a0e22660046757e03f59e38442727d5d4f5abd7884caaf6adb6f0babf6d0b8b9a4e6043904d7e186f3789e8d1f98
7
- data.tar.gz: cc7b0010c460da1b01b95d07f195650cec356e4fd9e779552ef16403fc183085459bb534dd95c6d23a4ac7c0ce68b0ba25f8909e74b25a79eb65885c208d5ac0
8
+ metadata.gz: !binary |-
9
+ Njc2ODUzMjc4YzE0NTczYmFmMDQ4MmNhZGRhZmMxZTFiNmNjNzdjZDE2OGQy
10
+ OTI3ZTlmZTNiOTVjZTAxNWY5NmUwZjc0ZDU2MjI2YTg0ZWEwZmEzODg3YzUz
11
+ MWY1ZGFiOTNkNDkxMTgxYmQxYWM4NTFiYTRhOGIxMzIxYzRhMWM=
12
+ data.tar.gz: !binary |-
13
+ Yjc2MGM5MmExMDNjMzY3ZmM4YWVkMTZjYzI0OTllZDFkNzkyZTRkMmQ5ZWQx
14
+ NGI3YWE3ZWIxN2M3MjliMDIzZTk4MmM1NGVmMzZkZWJjNzliYTQyMzA2MzJm
15
+ YTMwZjU4ZDc5ZGRiMjc5Mzc5ZTEwMGJmMDBjM2Q4ZjY1MGYwMDQ=
data/README.md CHANGED
@@ -2,20 +2,47 @@
2
2
 
3
3
  A web interface to Apache Cassandra with AngularJS and server-sent events.
4
4
 
5
- ## Quick Start
5
+ ## Installation
6
6
 
7
7
  ```bash
8
- gem install cassandra-web
9
- cassandra-web
8
+ $ gem install cassandra-web
10
9
  ```
11
10
 
12
- Run `cassandra-web -h` for futher help
11
+ ## Usage
12
+
13
+ ```bash
14
+ $ cassandra-web
15
+ ```
16
+
17
+ Run `cassandra-web -h` for futher help.
18
+
19
+ ## How it works
20
+
21
+ Cassandra web consists of an HTTP API powered by [Sinatra](https://github.com/sinatra/sinatra) and a thin HTML5/JavaScript frontend powered by [AngularJS](https://angularjs.org/).
22
+
23
+ When you run `cassandra-web` script, it starts a [Thin web server](http://code.macournoyer.com/thin/) on a specified address, which defaults to `localhost:3000`. Openning `http://localhost:3000`, or whatever address you've specified in the browser, loads the AngularJS application and it starts interacting with the HTTP API of `cassandra-web`. This api uses the [Ruby Driver](http://datastax.github.io/ruby-driver/) to communicate with an [Apache Cassandra](http://cassandra.apache.org/) cluster.
24
+
25
+ When the frontend has fully loaded, [it subscribes to `/events` API endpoint](https://github.com/avalanche123/cassandra-web/blob/master/app/public/js/cassandra.js#L108), and begins receiving [Server Sent Events](http://www.w3.org/TR/2012/WD-eventsource-20120426/). [The API uses an event listener, which is registered with the `Cluster` instance created by the Ruby Driver, to stream events](https://github.com/avalanche123/cassandra-web/blob/master/app/helpers/sse.rb#L43-L56) such as [schema](https://github.com/avalanche123/cassandra-web/blob/master/app/helpers/sse.rb#L29-L39) and [node status](https://github.com/avalanche123/cassandra-web/blob/master/app/helpers/sse.rb#L13-L27) changes to update the user interface without having to refresh the page.
26
+
27
+ You can see this feature in action by creating a keyspace using the execute button in the top-right corner of the UI and executing the following statement:
28
+
29
+ ```cql
30
+ CREATE KEYSPACE example WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}
31
+ ```
32
+
33
+ If the statement executed successfully, you should see a new keyspace show up on the left side of the UI.
34
+
35
+ ![Alt text](https://raw.githubusercontent.com/avalanche123/cassandra-web/master/animation.gif "Create Keyspace")
36
+
37
+ The web server, Thin, used by `cassandra-web` is asynchronous and uses only a single thread to handle requests. This enables efficient handling multiple of long running connections, which is a requirement for streaming and Server Sent Events, but also means that the application cannot perform blocking operations during request handling, since it would hang up all connections for the duration of the blocking operation. `cassandra-web` therefore uses Asynchronous Execution feature of the Ruby Driver to not block on statements execution. [The application executes statements asynchronously, receiving a future from the Ruby Driver](https://github.com/avalanche123/cassandra-web/blob/master/app.rb#L88). [It then registers future completion listeners to send a response (or error) whenever it becomes available](https://github.com/avalanche123/cassandra-web/blob/master/app/helpers/async.rb#L7-L40).
13
38
 
14
39
  ## Credits
15
40
 
16
41
  Cassandra web is possible because of the following awesome technologies (in no particular order):
17
42
 
18
43
  * [Apache Cassandra](http://cassandra.apache.org/)
44
+ * [DataStax Ruby Driver for Apache Cassandra](http://datastax.github.io/ruby-driver/)
45
+ * [Sinatra](https://github.com/sinatra/sinatra)
19
46
  * [AngularJS](https://angularjs.org/)
20
47
  * [Twitter Bootstrap](http://getbootstrap.com/)
21
48
  * [Thin](http://code.macournoyer.com/thin/)
data/app.rb CHANGED
@@ -43,7 +43,7 @@ class App < Sinatra::Base
43
43
  headers 'Connection' => 'keep-alive'
44
44
 
45
45
  stream(:keep_open) do |out|
46
- send_events(out)
46
+ stream_events(out)
47
47
  end
48
48
  end
49
49
 
@@ -71,33 +71,21 @@ class App < Sinatra::Base
71
71
  post '/execute/?' do
72
72
  content_type 'application/json'
73
73
 
74
- begin
75
- statement = params['statement']
76
- statement.strip!
77
- statement.chomp!(";")
78
-
79
- options = {
80
- :consistency => :one,
81
- :trace => false
82
- }
83
-
84
- if params['options']
85
- options[:trace] = !!params['options']['trace'] if params['options'].has_key?('trace')
86
- options[:consistency] = params['options']['consistency'].to_sym if params['options'].has_key?('consistency') && Cassandra::CONSISTENCIES.include?(params['options']['consistency'].to_sym)
87
- end
88
-
89
- status 200
90
- json_dump(session.execute(statement, options))
91
- rescue Cassandra::Errors::NoHostsAvailable => e
92
- status 503
93
- json_dump(e)
94
- rescue Cassandra::Errors::QueryError => e
95
- status 400
96
- json_dump(e)
97
- rescue => e
98
- status 500
99
- json_dump(e)
74
+ statement = params['statement']
75
+ statement.strip!
76
+ statement.chomp!(";")
77
+
78
+ options = {
79
+ :consistency => :one,
80
+ :trace => false
81
+ }
82
+
83
+ if params['options']
84
+ options[:trace] = !!params['options']['trace'] if params['options'].has_key?('trace')
85
+ options[:consistency] = params['options']['consistency'].to_sym if params['options'].has_key?('consistency') && Cassandra::CONSISTENCIES.include?(params['options']['consistency'].to_sym)
100
86
  end
87
+
88
+ defer(session.execute_async(statement, options))
101
89
  end
102
90
 
103
91
  get '*' do
@@ -105,8 +93,10 @@ class App < Sinatra::Base
105
93
  end
106
94
  end
107
95
 
96
+ require 'app/helpers/async'
108
97
  require 'app/helpers/json'
109
98
  require 'app/helpers/sse'
110
99
 
100
+ App.helpers App::Helpers::Async
111
101
  App.helpers App::Helpers::JSON
112
102
  App.helpers App::Helpers::SSE
@@ -0,0 +1,46 @@
1
+ # encoding: utf-8
2
+
3
+ class App
4
+ module Helpers
5
+ module Async
6
+ def defer(future)
7
+ future.on_success do |result|
8
+ env["async.callback"].call([
9
+ 200,
10
+ {
11
+ 'Content-Type' => 'application/json'
12
+ },
13
+ [json_dump(result)]
14
+ ])
15
+ end
16
+
17
+ future.on_failure do |error|
18
+ status = case error
19
+ when Cassandra::Errors::NoHostsAvailable
20
+ 503
21
+ when Cassandra::Errors::AuthenticationError
22
+ 401
23
+ when Cassandra::Errors::UnauthorizedError
24
+ 403
25
+ when Cassandra::Errors::ExecutionError
26
+ 504
27
+ when Cassandra::Error
28
+ 400
29
+ when
30
+ 500
31
+ end
32
+
33
+ env["async.callback"].call([
34
+ status,
35
+ {
36
+ 'Content-Type' => 'application/json'
37
+ },
38
+ [json_dump(error)]
39
+ ])
40
+ end
41
+
42
+ throw :async
43
+ end
44
+ end
45
+ end
46
+ end
data/app/helpers/json.rb CHANGED
@@ -17,12 +17,8 @@ class App
17
17
  keyspace_hash(object)
18
18
  when ::Cassandra::Result
19
19
  result_hash(object)
20
- when ::Cassandra::Errors::NoHostsAvailable
21
- no_hosts_available_error_hash(object)
22
- when ::Cassandra::Errors::QueryError
23
- query_error_hash(object)
24
20
  when ::Exception
25
- error_hash(object)
21
+ exception_hash(object)
26
22
  when ::Hash
27
23
  hash = ::Hash.new
28
24
  object.each do |key, value|
@@ -98,14 +94,14 @@ class App
98
94
  def result_hash(result)
99
95
  {
100
96
  :rows => result.map(&method(:object_hash)),
101
- :columns => columns_hash(result.first),
97
+ :columns => columns_hash(result),
102
98
  :info => execution_info_hash(result.execution_info),
103
99
  }
104
100
  end
105
101
 
106
- def columns_hash(row)
107
- return [] if row.nil?
108
- row.keys
102
+ def columns_hash(rows)
103
+ return [] if rows.empty?
104
+ rows.first.keys
109
105
  end
110
106
 
111
107
  def execution_info_hash(execution_info)
@@ -159,19 +155,92 @@ class App
159
155
  end
160
156
 
161
157
  def error_hash(error)
158
+ case error
159
+ when ::Cassandra::Errors::NoHostsAvailable
160
+ no_hosts_available_error_hash(error)
161
+ when ::Cassandra::Errors::ReadTimeoutError
162
+ read_timeout_error_hash(error)
163
+ when ::Cassandra::Errors::WriteTimeoutError
164
+ write_timeout_error_hash(error)
165
+ when ::Cassandra::Errors::UnavailableError
166
+ unavailable_error_hash(error)
167
+ when ::Cassandra::Errors::UnpreparedError
168
+ unprepared_error_hash(error)
169
+ when ::Cassandra::Errors::AlreadyExistsError
170
+ already_exists_error_hash(error)
171
+ when ::Cassandra::Errors::ExecutionError, ::Cassandra::Errors::ValidationError
172
+ execution_error_hash(error)
173
+ else
174
+ exception_hash(error)
175
+ end
176
+ end
177
+
178
+ def exception_hash(error)
162
179
  {
163
180
  :class => error.class.name,
164
181
  :message => error.message,
165
- :trace => error.backtrace
182
+ :trace => error.backtrace
166
183
  }
167
184
  end
168
185
 
169
- def query_error_hash(error)
170
- error_hash(error)
186
+ def execution_error_hash(error)
187
+ hash = exception_hash(error)
188
+ hash[:statement] = statement_hash(error.statement)
189
+ hash
190
+ end
191
+
192
+ def already_exists_error_hash(error)
193
+ hash = execution_error_hash(error)
194
+ hash[:keyspace] = error.keyspace
195
+ hash[:table] = error.table
196
+ hash
197
+ end
198
+
199
+ def unprepared_error_hash(error)
200
+ hash = execution_error_hash(error)
201
+ hash[:id] = error.id
202
+ hash
203
+ end
204
+
205
+ def read_timeout_error_hash(error)
206
+ hash = execution_error_hash(error)
207
+ hash[:retrieved] = error.retrieved?
208
+ hash[:consistency] = error.consistency
209
+ hash[:required] = error.required
210
+ hash[:received] = error.received
211
+ hash
212
+ end
213
+
214
+ def write_timeout_error_hash(error)
215
+ hash = execution_error_hash(error)
216
+ hash[:type] = error.type
217
+ hash[:consistency] = error.consistency
218
+ hash[:required] = error.required
219
+ hash[:received] = error.received
220
+ hash
221
+ end
222
+
223
+ def unavailable_error_hash(error)
224
+ hash = execution_error_hash(error)
225
+ hash[:consistency] = error.consistency
226
+ hash[:required] = error.required
227
+ hash[:alive] = error.alive
228
+ hash
171
229
  end
172
230
 
173
231
  def no_hosts_available_error_hash(error)
174
- error_hash(error)
232
+ hash = exception_hash(error)
233
+ errors = []
234
+
235
+ error.errors.each do |host, e|
236
+ errors << {
237
+ :host => host_hash(host),
238
+ :error => error_hash(e)
239
+ }
240
+ end
241
+
242
+ hash[:errors] = errors
243
+ hash
175
244
  end
176
245
  end
177
246
  end
data/app/helpers/sse.rb CHANGED
@@ -39,7 +39,7 @@ class App
39
39
  end
40
40
  end
41
41
 
42
- def send_events(out)
42
+ def stream_events(out)
43
43
  listener = Streamer.new(out)
44
44
  heartbeat = EM.add_periodic_timer(2) { out << "\n" }
45
45
 
data/bin/cassandra-web CHANGED
@@ -21,16 +21,20 @@ class CLI
21
21
  @parser = OptionParser.new
22
22
  @options = {
23
23
  :bind => '0.0.0.0:3000',
24
- :log_level => 'debug'
24
+ :log_level => 'info'
25
25
  }
26
26
 
27
- option(:bind, '-B', '--bind BIND', String, 'ip:port or path for cassandra web to bind on')
28
- option(:hosts, '-H', '--hosts HOSTS', String, 'coma-separated list of cassandra hosts')
29
- option(:port, '-P', '--port PORT', Integer, 'integer port that cassandra is running on')
27
+ option(:bind, '-B', '--bind BIND', String, 'ip:port or path for cassandra web to bind on (default: 0.0.0.0:3000)')
28
+ option(:hosts, '-H', '--hosts HOSTS', String, 'coma-separated list of cassandra hosts (default: 127.0.0.1)')
29
+ option(:port, '-P', '--port PORT', Integer, 'integer port that cassandra is running on (default: 9042)')
30
+ option(:log_level, '-L', '--log-level LEVEL', String, 'log level (default: info)')
30
31
  option(:username, '-u', '--username USER', String, 'username to use when connecting to cassandra')
31
32
  option(:password, '-p', '--password PASS', String, 'password to use when connecting to cassandra')
32
33
  option(:compression, '-C', '--compression NAME', String, 'compression algorithm to use (lz4 or snappy)')
33
- option(:log_level, '-L', '--log-level LEVEL', String, 'log level')
34
+ option(:server_cert, '--server-cert PATH', String, 'server ceritificate pathname')
35
+ option(:client_cert, '--client-cert PATH', String, 'client ceritificate pathname')
36
+ option(:private_key, '--private-key PATH', String, 'path to private key')
37
+ option(:passphrase, '--passphrase SECRET', String, 'passphrase for the private key')
34
38
 
35
39
  @parser.on('-h', '--help', 'Show help') { show_help }
36
40
  end
@@ -45,14 +49,17 @@ class CLI
45
49
 
46
50
  @options.each do |name, value|
47
51
  value = case name
48
- when :hosts then value.split(',').map!(&:strip)
49
- when :compression then value.downcase.to_sym
50
- when :port, :username, :password
52
+ when :port, :username, :password # skip as is
53
+ when :hosts
54
+ value.split(',').map!(&:strip)
55
+ when :compression
56
+ value.downcase.to_sym
51
57
  when :log_level
52
- name = :logger
53
- Logger.new(@out).tap do |logger|
54
- logger.level = Logger.const_get(value.upcase.to_sym)
55
- end
58
+ name = :logger
59
+
60
+ logger = Logger.new(@out)
61
+ logger.level = Logger.const_get(value.upcase.to_sym)
62
+ logger
56
63
  else
57
64
  next
58
65
  end
@@ -60,7 +67,14 @@ class CLI
60
67
  options[name] = value
61
68
  end
62
69
 
63
- cluster = ::Cassandra.connect(options)
70
+ hosts = Array(options[:hosts])
71
+ hosts << '127.0.0.1' if hosts.empty?
72
+
73
+ options[:load_balancing_policy] = ::Cassandra::LoadBalancing::Policies::WhiteList.new(hosts, ::Cassandra::LoadBalancing::Policies::RoundRobin.new)
74
+ options[:compression] = :lz4
75
+ options[:page_size] = nil
76
+
77
+ cluster = ::Cassandra.cluster(options)
64
78
 
65
79
  App.set(:cluster, cluster)
66
80
  App.set(:session, cluster.connect)
metadata CHANGED
@@ -1,113 +1,127 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cassandra-web
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bulat Shakirzyanov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-07 00:00:00.000000000 Z
11
+ date: 2014-12-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
+ name: cassandra-driver
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ type: :runtime
14
21
  prerelease: false
15
22
  version_requirements: !ruby/object:Gem::Requirement
16
23
  requirements:
17
24
  - - ~>
18
25
  - !ruby/object:Gem::Version
19
- version: 1.0.0.beta
20
- type: :runtime
26
+ version: '1.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: thin
21
29
  requirement: !ruby/object:Gem::Requirement
22
30
  requirements:
23
31
  - - ~>
24
32
  - !ruby/object:Gem::Version
25
- version: 1.0.0.beta
26
- name: cassandra-driver
27
- - !ruby/object:Gem::Dependency
33
+ version: '1.6'
34
+ type: :runtime
28
35
  prerelease: false
29
36
  version_requirements: !ruby/object:Gem::Requirement
30
37
  requirements:
31
38
  - - ~>
32
39
  - !ruby/object:Gem::Version
33
40
  version: '1.6'
34
- type: :runtime
41
+ - !ruby/object:Gem::Dependency
42
+ name: rack-cors
35
43
  requirement: !ruby/object:Gem::Requirement
36
44
  requirements:
37
45
  - - ~>
38
46
  - !ruby/object:Gem::Version
39
- version: '1.6'
40
- name: thin
41
- - !ruby/object:Gem::Dependency
47
+ version: '0.2'
48
+ type: :runtime
42
49
  prerelease: false
43
50
  version_requirements: !ruby/object:Gem::Requirement
44
51
  requirements:
45
52
  - - ~>
46
53
  - !ruby/object:Gem::Version
47
54
  version: '0.2'
48
- type: :runtime
55
+ - !ruby/object:Gem::Dependency
56
+ name: rack-parser
49
57
  requirement: !ruby/object:Gem::Requirement
50
58
  requirements:
51
59
  - - ~>
52
60
  - !ruby/object:Gem::Version
53
- version: '0.2'
54
- name: rack-cors
55
- - !ruby/object:Gem::Dependency
61
+ version: '0.6'
62
+ type: :runtime
56
63
  prerelease: false
57
64
  version_requirements: !ruby/object:Gem::Requirement
58
65
  requirements:
59
66
  - - ~>
60
67
  - !ruby/object:Gem::Version
61
68
  version: '0.6'
62
- type: :runtime
69
+ - !ruby/object:Gem::Dependency
70
+ name: sinatra
63
71
  requirement: !ruby/object:Gem::Requirement
64
72
  requirements:
65
73
  - - ~>
66
74
  - !ruby/object:Gem::Version
67
- version: '0.6'
68
- name: rack-parser
69
- - !ruby/object:Gem::Dependency
75
+ version: '1.4'
76
+ type: :runtime
70
77
  prerelease: false
71
78
  version_requirements: !ruby/object:Gem::Requirement
72
79
  requirements:
73
80
  - - ~>
74
81
  - !ruby/object:Gem::Version
75
82
  version: '1.4'
76
- type: :runtime
83
+ - !ruby/object:Gem::Dependency
84
+ name: lz4-ruby
77
85
  requirement: !ruby/object:Gem::Requirement
78
86
  requirements:
79
87
  - - ~>
80
88
  - !ruby/object:Gem::Version
81
- version: '1.4'
82
- name: sinatra
83
- - !ruby/object:Gem::Dependency
89
+ version: '0.3'
90
+ type: :runtime
84
91
  prerelease: false
85
92
  version_requirements: !ruby/object:Gem::Requirement
86
93
  requirements:
87
94
  - - ~>
88
95
  - !ruby/object:Gem::Version
89
- version: '1.6'
90
- type: :development
96
+ version: '0.3'
97
+ - !ruby/object:Gem::Dependency
98
+ name: bundler
91
99
  requirement: !ruby/object:Gem::Requirement
92
100
  requirements:
93
101
  - - ~>
94
102
  - !ruby/object:Gem::Version
95
103
  version: '1.6'
96
- name: bundler
97
- - !ruby/object:Gem::Dependency
104
+ type: :development
98
105
  prerelease: false
99
106
  version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ version: '1.6'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
100
114
  requirements:
101
115
  - - ~>
102
116
  - !ruby/object:Gem::Version
103
117
  version: '10.0'
104
118
  type: :development
105
- requirement: !ruby/object:Gem::Requirement
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
106
121
  requirements:
107
122
  - - ~>
108
123
  - !ruby/object:Gem::Version
109
124
  version: '10.0'
110
- name: rake
111
125
  description: Apache Cassandra web interface using Ruby, Event-machine, AngularJS,
112
126
  Server-Sent-Events and DataStax Ruby driver for Apache Cassandra
113
127
  email:
@@ -119,6 +133,7 @@ extra_rdoc_files: []
119
133
  files:
120
134
  - README.md
121
135
  - app.rb
136
+ - app/helpers/async.rb
122
137
  - app/helpers/json.rb
123
138
  - app/helpers/sse.rb
124
139
  - app/public/css/bootstrap-theme.css
@@ -156,17 +171,17 @@ require_paths:
156
171
  - lib
157
172
  required_ruby_version: !ruby/object:Gem::Requirement
158
173
  requirements:
159
- - - '>='
174
+ - - ! '>='
160
175
  - !ruby/object:Gem::Version
161
176
  version: 1.9.3
162
177
  required_rubygems_version: !ruby/object:Gem::Requirement
163
178
  requirements:
164
- - - '>='
179
+ - - ! '>='
165
180
  - !ruby/object:Gem::Version
166
181
  version: '0'
167
182
  requirements: []
168
183
  rubyforge_project:
169
- rubygems_version: 2.4.1
184
+ rubygems_version: 2.4.2
170
185
  signing_key:
171
186
  specification_version: 4
172
187
  summary: A simple web ui for Apache Cassandra