ilios 1.1.1 → 1.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b605b28e1524aca634a188b28f9f557745673a789912e4971f173f5687d7ef6
4
- data.tar.gz: 46cc89d6f8f096f926339f2e9d14e76b72858167a9332d4e53ba1800fe10f527
3
+ metadata.gz: dc846b97514ac6b86eccf9a06eee87d35396f26746974e6927d7d99f0bcf33ed
4
+ data.tar.gz: cd91086e384b4af2fc3a39cb0c5234722947dbb8dc097782a4508df9bf73615f
5
5
  SHA512:
6
- metadata.gz: d7008c470792420f2ff04f03c7d48d4f267a89de816f90d1381ccef7f0c19a7a5623067c916ba58c2a3dd14e4698e68822313692b0a7e19f1e6275803297e4ce
7
- data.tar.gz: 2c073c1b1b2e8227579bfd5b9e1847d4124f379615e236bf573a15a971aa55a9a26e920efa3f8e857363465cab24af2d17e49730a33043a45299f0907d81dbd6
6
+ metadata.gz: bf94440639db92113db0a8c06a05f2409139644268988aa7c2c94f202bab494d035fd6107596010f07222feecbe92cad8c3f14ed169061d16d91fe69db8f05f9
7
+ data.tar.gz: 2b514895a3a71380e5fab10198a92e1dc1a9cdea7d84d10dad344658f14efbc7884c8fa49e3c80bcd9a8cb7942f0cb1d846a7e92cfca9ae02ff8b785989c6db5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.2.0
4
+
5
+ - Fix cluster setters silently ignoring invalid values in `Cluster#port`, `#protocol_version`, `#connect_timeout`, `#request_timeout` and `#resolve_timeout` (#36)
6
+ - Fix segfault when a `Cluster` created by `allocate` or `dup` is used without running `initialize` (#37)
7
+ - Yield the failure reason as an `Ilios::Cassandra::ExecutionError` to `Future#on_failure` blocks that accept an argument, including variadic blocks (e.g. `{ |*args| }`); zero-arity blocks are unchanged (#40)
8
+ - The synchronous API (`Session#prepare`, `Session#execute`, `Result#next_page`, `Cluster#connect`) now raises errors carrying the server-reported message and a `#code` Integer, same as `Future#on_failure` (#40)
9
+ - `Ilios::Cassandra::StatementError` also carries a `#code` Integer, so `#code` is answered by every error class the driver raises (#40)
10
+ - Fix `Future#on_success` RBS signature to allow the `Statement` yielded by `prepare_async` (#42)
11
+
12
+ ## 1.1.2
13
+
14
+ - Support username/password authentication with `Cluster#credentials` (#33)
15
+ - Add cluster options: consistency, serial_consistency, num_threads_io, queue_size_io, core_connections_per_host, constant_reconnect, exponential_reconnect, tcp_nodelay, tcp_keepalive, connection_heartbeat_interval, connection_idle_timeout, load_balance_round_robin, load_balance_dc_aware, token_aware_routing, latency_aware_routing and use_schema (#33)
16
+ - Accept Symbol consistency levels (e.g. `:quorum`) in `Cluster#consistency` and `Cluster#serial_consistency` (#33)
17
+ - Fix infinite loop when binding a `list`/`set` whose element appends to the array while it is being converted (#34)
18
+ - Fix infinite loop in `Cluster#hosts` when an element appends to the array while it is being converted (#34)
19
+
3
20
  ## 1.1.1
4
21
 
5
22
  - Fix out-of-bounds read when binding a `list`/`set` whose element mutates the array while it is being converted (#31)
data/README.md CHANGED
@@ -171,6 +171,25 @@ Notes:
171
171
  - Because a `Symbol` map key is stored as its `String` equivalent, binding a map that contains both (for example `{ k1: 1, 'k1' => 2 }`) ends up as a single key on the server; the entry bound last wins.
172
172
  - Binding a `String` containing a NUL character (`\0`) to a `text` (or `ascii` / `varchar`) column raises `ArgumentError` (known limitation).
173
173
 
174
+ ### Authentication and cluster options
175
+
176
+ Username/password authentication (Cassandra's `PasswordAuthenticator`) and
177
+ common cluster options are configured on `Cluster` before `connect`:
178
+
179
+ ```ruby
180
+ cluster = Ilios::Cassandra::Cluster.new
181
+ cluster.hosts(['127.0.0.1'])
182
+ cluster.credentials('username', 'password')
183
+ cluster.consistency(:quorum) # or Ilios::Cassandra::Cluster::CONSISTENCY_QUORUM
184
+ cluster.num_threads_io(4)
185
+ cluster.exponential_reconnect(2_000, 60_000)
186
+ cluster.tcp_keepalive(true, 60)
187
+ cluster.load_balance_dc_aware('dc1')
188
+ session = cluster.connect
189
+ ```
190
+
191
+ See the RBS signatures in `sig/ilios.rbs` for the full list of options.
192
+
174
193
  ### Synchronous API
175
194
  `Ilios::Cassandra::Session#prepare` and `Ilios::Cassandra::Session#execute` are provided as synchronous API.
176
195
 
@@ -207,8 +226,12 @@ prepare_future.on_success { |statement|
207
226
  p result
208
227
  p "success"
209
228
  }
210
- result_future.on_failure {
211
- p "fail"
229
+ # `error` is an `Ilios::Cassandra::ExecutionError` (a StandardError with
230
+ # a #code Integer attribute); the block is optional-arity, so `{ p "fail" }`
231
+ # still works without it.
232
+ result_future.on_failure { |error|
233
+ p error.message
234
+ p error.code
212
235
  }
213
236
 
214
237
  futures << result_future
data/Rakefile CHANGED
@@ -4,9 +4,6 @@ require 'bundler/gem_tasks'
4
4
  require 'rake/extensiontask'
5
5
  require 'rake/testtask'
6
6
 
7
- desc 'Run tests'
8
- task test: :compile
9
-
10
7
  task default: :test
11
8
 
12
9
  Rake::ExtensionTask.new('ilios') do |ext|
@@ -14,9 +11,10 @@ Rake::ExtensionTask.new('ilios') do |ext|
14
11
  end
15
12
 
16
13
  test_config = lambda do |t|
14
+ t.deps = [:compile]
17
15
  t.pattern = 'test/test_*.rb'
18
16
  end
19
- Rake::TestTask.new(test: :compile, &test_config)
17
+ Rake::TestTask.new(:test, &test_config)
20
18
 
21
19
  namespace :rbs do
22
20
  desc 'Validate RBS definitions'
@@ -31,7 +29,9 @@ end
31
29
  if RUBY_PLATFORM.include?('linux')
32
30
  require 'ruby_memcheck'
33
31
 
32
+ RubyMemcheck.config(binary_name: 'ilios')
33
+
34
34
  namespace :test do
35
- RubyMemcheck::TestTask.new(valgrind: :compile, &test_config)
35
+ RubyMemcheck::TestTask.new(:valgrind, &test_config)
36
36
  end
37
37
  end
data/docker-compose.yml CHANGED
@@ -6,17 +6,32 @@ services:
6
6
  - 9042:9042
7
7
  volumes:
8
8
  - cassandra-data:/var/lib/cassandra
9
+ cassandra-auth:
10
+ image: cassandra:4
11
+ ports:
12
+ - "9043:9042"
13
+ command: >
14
+ bash -c "sed -i 's/^authenticator:.*/authenticator: PasswordAuthenticator/'
15
+ /etc/cassandra/cassandra.yaml
16
+ && exec docker-entrypoint.sh cassandra -f"
17
+ volumes:
18
+ - cassandra-auth-data:/var/lib/cassandra
9
19
  ilios:
10
20
  build:
11
21
  context: ./dockerfiles
12
22
  dockerfile: ubuntu.dockerfile
13
23
  depends_on:
14
24
  - cassandra
25
+ - cassandra-auth
15
26
  environment:
16
27
  CASSANDRA_HOST: cassandra
28
+ CASSANDRA_AUTH_HOST: cassandra-auth
29
+ CASSANDRA_AUTH_PORT: 9042
17
30
  command: bash -c 'sleep infinity'
18
31
  volumes:
19
32
  - "./:/opt/ilios"
20
33
  volumes:
21
34
  cassandra-data:
22
35
  driver: local
36
+ cassandra-auth-data:
37
+ driver: local
data/ext/ilios/cluster.c CHANGED
@@ -23,6 +23,36 @@ static VALUE cluster_allocator(VALUE klass)
23
23
  return CREATE_CLUSTER(cassandra_cluster);
24
24
  }
25
25
 
26
+ static void cluster_check_error(CassError error, const char *name)
27
+ {
28
+ if (error != CASS_OK) {
29
+ rb_raise(rb_eArgError, "Invalid %s: %s", name, cass_error_desc(error));
30
+ }
31
+ }
32
+
33
+ // Casting a negative value to unsigned wraps it into a huge number, so check
34
+ // the range on the signed value NUM2LONG/NUM2LL already produced. Converting a
35
+ // second time would run to_int again, which may return a different value.
36
+ static unsigned cluster_value_to_uint(VALUE value, const char *name)
37
+ {
38
+ long v = NUM2LONG(value);
39
+
40
+ if (v < 0 || v > UINT_MAX) {
41
+ rb_raise(rb_eRangeError, "Invalid %s: %ld", name, v);
42
+ }
43
+ return (unsigned)v;
44
+ }
45
+
46
+ static cass_uint64_t cluster_value_to_uint64(VALUE value, const char *name)
47
+ {
48
+ long long v = NUM2LL(value);
49
+
50
+ if (v < 0) {
51
+ rb_raise(rb_eRangeError, "Invalid %s: %lld", name, v);
52
+ }
53
+ return (cass_uint64_t)v;
54
+ }
55
+
26
56
  /**
27
57
  * Creates a new cluster.
28
58
  *
@@ -32,7 +62,7 @@ static VALUE cluster_initialize(VALUE self)
32
62
  {
33
63
  CassandraCluster *cassandra_cluster;
34
64
 
35
- GET_CLUSTER(self, cassandra_cluster);
65
+ GET_UNINITIALIZED_CLUSTER(self, cassandra_cluster);
36
66
  cassandra_cluster->cluster = cass_cluster_new();
37
67
 
38
68
  return self;
@@ -65,12 +95,10 @@ static VALUE cluster_connect(VALUE self)
65
95
  nogvl_future_wait(connect_future);
66
96
 
67
97
  if (cass_future_error_code(connect_future) != CASS_OK) {
68
- char error[4096] = { 0 };
98
+ VALUE error = ilios_future_error_new(eConnectError, "Unable to connect", connect_future);
69
99
 
70
- strncpy(error, cass_error_desc(cass_future_error_code(connect_future)), sizeof(error) - 1);
71
100
  cass_future_free(connect_future);
72
- rb_raise(eConnectError, "Unable to connect: %s", error);
73
- return Qnil;
101
+ rb_exc_raise(error);
74
102
  }
75
103
  cass_future_free(connect_future);
76
104
 
@@ -86,16 +114,18 @@ static VALUE cluster_connect(VALUE self)
86
114
  static VALUE cluster_hosts(VALUE self, VALUE hosts)
87
115
  {
88
116
  CassandraCluster *cassandra_cluster;
117
+ long length;
89
118
 
90
119
  GET_CLUSTER(self, cassandra_cluster);
91
120
 
92
121
  Check_Type(hosts, T_ARRAY);
93
- if (RARRAY_LEN(hosts) == 0) {
122
+ length = RARRAY_LEN(hosts);
123
+ if (length == 0) {
94
124
  rb_raise(rb_eArgError, "No host exists.");
95
125
  }
96
126
 
97
- for (int i = 0; i < RARRAY_LEN(hosts); i++) {
98
- VALUE host = RARRAY_AREF(hosts, i);
127
+ for (long i = 0; i < length && i < RARRAY_LEN(hosts); i++) {
128
+ VALUE host = rb_ary_entry(hosts, i);
99
129
  cass_cluster_set_contact_points(cassandra_cluster->cluster, StringValueCStr(host));
100
130
  }
101
131
 
@@ -108,13 +138,14 @@ static VALUE cluster_hosts(VALUE self, VALUE hosts)
108
138
  *
109
139
  * @param port [Integer] A port number.
110
140
  * @return [Cassandra::Cluster] self.
141
+ * @raise [ArgumentError] If the port is zero or negative.
111
142
  */
112
143
  static VALUE cluster_port(VALUE self, VALUE port)
113
144
  {
114
145
  CassandraCluster *cassandra_cluster;
115
146
 
116
147
  GET_CLUSTER(self, cassandra_cluster);
117
- cass_cluster_set_port(cassandra_cluster->cluster, NUM2INT(port));
148
+ cluster_check_error(cass_cluster_set_port(cassandra_cluster->cluster, NUM2INT(port)), "port");
118
149
 
119
150
  return self;
120
151
  }
@@ -139,17 +170,30 @@ static VALUE cluster_keyspace(VALUE self, VALUE keyspace)
139
170
 
140
171
  /**
141
172
  * Sets the protocol version. The driver will automatically downgrade to the lowest supported protocol version.
142
- * Default is +PROTOCOL_VERSION_V4+.
173
+ * Default is +PROTOCOL_VERSION_V4+, or +PROTOCOL_VERSION_DSEV1+ against DSE.
143
174
  *
144
- * @param timeout_ms [Integer] A connect timeout in milliseconds.
175
+ * The driver accepts +PROTOCOL_VERSION_V3+, +PROTOCOL_VERSION_V4+,
176
+ * +PROTOCOL_VERSION_DSEV1+ and +PROTOCOL_VERSION_DSEV2+ only. It ignores
177
+ * +PROTOCOL_VERSION_V1+ and +PROTOCOL_VERSION_V2+, which are below the lowest
178
+ * version it supports, and +PROTOCOL_VERSION_V5+, which is a beta version it
179
+ * enables through a separate setting instead. Each of those leaves the version
180
+ * unchanged and is reported on the driver's error log.
181
+ *
182
+ * @param version [Integer] A protocol version.
145
183
  * @return [Cassandra::Cluster] self.
184
+ * @raise [RangeError] If a negative value was given.
146
185
  */
147
186
  static VALUE cluster_protocol_version(VALUE self, VALUE version)
148
187
  {
149
188
  CassandraCluster *cassandra_cluster;
189
+ int v = NUM2INT(version);
190
+
191
+ if (v < 0) {
192
+ rb_raise(rb_eRangeError, "Invalid protocol_version: %d", v);
193
+ }
150
194
 
151
195
  GET_CLUSTER(self, cassandra_cluster);
152
- cass_cluster_set_protocol_version(cassandra_cluster->cluster, NUM2INT(version));
196
+ cass_cluster_set_protocol_version(cassandra_cluster->cluster, v);
153
197
 
154
198
  return self;
155
199
  }
@@ -160,13 +204,14 @@ static VALUE cluster_protocol_version(VALUE self, VALUE version)
160
204
  *
161
205
  * @param timeout_ms [Integer] A connect timeout in milliseconds.
162
206
  * @return [Cassandra::Cluster] self.
207
+ * @raise [RangeError] If a negative value was given.
163
208
  */
164
209
  static VALUE cluster_connect_timeout(VALUE self, VALUE timeout_ms)
165
210
  {
166
211
  CassandraCluster *cassandra_cluster;
167
212
 
168
213
  GET_CLUSTER(self, cassandra_cluster);
169
- cass_cluster_set_connect_timeout(cassandra_cluster->cluster, NUM2UINT(timeout_ms));
214
+ cass_cluster_set_connect_timeout(cassandra_cluster->cluster, cluster_value_to_uint(timeout_ms, "connect_timeout"));
170
215
 
171
216
  return self;
172
217
  }
@@ -177,13 +222,14 @@ static VALUE cluster_connect_timeout(VALUE self, VALUE timeout_ms)
177
222
  *
178
223
  * @param timeout_ms [Integer] A request timeout in milliseconds.
179
224
  * @return [Cassandra::Cluster] self.
225
+ * @raise [RangeError] If a negative value was given.
180
226
  */
181
227
  static VALUE cluster_request_timeout(VALUE self, VALUE timeout_ms)
182
228
  {
183
229
  CassandraCluster *cassandra_cluster;
184
230
 
185
231
  GET_CLUSTER(self, cassandra_cluster);
186
- cass_cluster_set_request_timeout(cassandra_cluster->cluster, NUM2UINT(timeout_ms));
232
+ cass_cluster_set_request_timeout(cassandra_cluster->cluster, cluster_value_to_uint(timeout_ms, "request_timeout"));
187
233
 
188
234
  return self;
189
235
  }
@@ -194,13 +240,14 @@ static VALUE cluster_request_timeout(VALUE self, VALUE timeout_ms)
194
240
  *
195
241
  * @param timeout_ms [Integer] A request timeout in milliseconds.
196
242
  * @return [Cassandra::Cluster] self.
243
+ * @raise [RangeError] If a negative value was given.
197
244
  */
198
245
  static VALUE cluster_resolve_timeout(VALUE self, VALUE timeout_ms)
199
246
  {
200
247
  CassandraCluster *cassandra_cluster;
201
248
 
202
249
  GET_CLUSTER(self, cassandra_cluster);
203
- cass_cluster_set_resolve_timeout(cassandra_cluster->cluster, NUM2UINT(timeout_ms));
250
+ cass_cluster_set_resolve_timeout(cassandra_cluster->cluster, cluster_value_to_uint(timeout_ms, "resolve_timeout"));
204
251
 
205
252
  return self;
206
253
  }
@@ -215,13 +262,407 @@ static VALUE cluster_resolve_timeout(VALUE self, VALUE timeout_ms)
215
262
  static VALUE cluster_constant_speculative_execution_policy(VALUE self, VALUE constant_delay_ms, VALUE max_speculative_executions)
216
263
  {
217
264
  CassandraCluster *cassandra_cluster;
265
+ long delay = NUM2LONG(constant_delay_ms);
266
+ int max_executions = NUM2INT(max_speculative_executions);
218
267
 
219
- if (NUM2LONG(constant_delay_ms) < 0 || NUM2INT(max_speculative_executions) < 0) {
268
+ if (delay < 0 || max_executions < 0) {
220
269
  rb_raise(rb_eArgError, "Bad parameters.");
221
270
  }
222
271
 
223
272
  GET_CLUSTER(self, cassandra_cluster);
224
- cass_cluster_set_constant_speculative_execution_policy(cassandra_cluster->cluster, NUM2LONG(constant_delay_ms), NUM2INT(max_speculative_executions));
273
+ cluster_check_error(cass_cluster_set_constant_speculative_execution_policy(cassandra_cluster->cluster, delay, max_executions), "constant_speculative_execution_policy");
274
+
275
+ return self;
276
+ }
277
+
278
+ /**
279
+ * Sets credentials for plain text authentication.
280
+ *
281
+ * @param username [String] A username.
282
+ * @param password [String] A password.
283
+ * @return [Cassandra::Cluster] self.
284
+ */
285
+ static VALUE cluster_credentials(VALUE self, VALUE username, VALUE password)
286
+ {
287
+ CassandraCluster *cassandra_cluster;
288
+
289
+ GET_CLUSTER(self, cassandra_cluster);
290
+ // The driver copies both strings into its own memory
291
+ // (cluster_config.cpp: cass_cluster_set_credentials_n), so the Ruby
292
+ // strings do not need to be retained.
293
+ cass_cluster_set_credentials(cassandra_cluster->cluster, StringValueCStr(username), StringValueCStr(password));
294
+
295
+ return self;
296
+ }
297
+
298
+ // The driver only rejects CASS_CONSISTENCY_UNKNOWN (0xFFFF) at set time; any
299
+ // other out-of-range value is accepted and later fails every query with an
300
+ // opaque protocol error, so validate the range here. This also keeps UNKNOWN
301
+ // away from the driver, which is why the setters ignore its return value.
302
+ static void cluster_check_consistency(long value, const char *name)
303
+ {
304
+ if (value < CASS_CONSISTENCY_ANY || value > CASS_CONSISTENCY_LOCAL_ONE) {
305
+ rb_raise(rb_eArgError, "Invalid %s: %ld", name, value);
306
+ }
307
+ }
308
+
309
+ static struct {
310
+ const char *name;
311
+ CassConsistency consistency;
312
+ ID id;
313
+ } cluster_consistency_symbols[] = {
314
+ { "any", CASS_CONSISTENCY_ANY, 0 },
315
+ { "one", CASS_CONSISTENCY_ONE, 0 },
316
+ { "two", CASS_CONSISTENCY_TWO, 0 },
317
+ { "three", CASS_CONSISTENCY_THREE, 0 },
318
+ { "quorum", CASS_CONSISTENCY_QUORUM, 0 },
319
+ { "all", CASS_CONSISTENCY_ALL, 0 },
320
+ { "local_quorum", CASS_CONSISTENCY_LOCAL_QUORUM, 0 },
321
+ { "each_quorum", CASS_CONSISTENCY_EACH_QUORUM, 0 },
322
+ { "serial", CASS_CONSISTENCY_SERIAL, 0 },
323
+ { "local_serial", CASS_CONSISTENCY_LOCAL_SERIAL, 0 },
324
+ { "local_one", CASS_CONSISTENCY_LOCAL_ONE, 0 },
325
+ };
326
+
327
+ // Accepts either an Integer consistency constant or a Symbol such as
328
+ // +:quorum+/+:local_serial+ naming one of the CONSISTENCY_* constants.
329
+ static CassConsistency cluster_value_to_consistency(VALUE value, const char *name)
330
+ {
331
+ long v;
332
+
333
+ if (SYMBOL_P(value)) {
334
+ // rb_sym2id would intern an unknown Symbol and leave it uncollectable, so
335
+ // repeated invalid values grow memory with no way to reclaim it.
336
+ VALUE name_value = value;
337
+ ID id = rb_check_id(&name_value);
338
+
339
+ for (size_t i = 0; id && i < sizeof(cluster_consistency_symbols) / sizeof(cluster_consistency_symbols[0]); i++) {
340
+ if (id == cluster_consistency_symbols[i].id) {
341
+ return cluster_consistency_symbols[i].consistency;
342
+ }
343
+ }
344
+ rb_raise(rb_eArgError, "Invalid %s: %"PRIsVALUE"", name, value);
345
+ }
346
+
347
+ v = NUM2LONG(value);
348
+ cluster_check_consistency(v, name);
349
+ return (CassConsistency)v;
350
+ }
351
+
352
+ /**
353
+ * Sets the default consistency level of the statement.
354
+ * Default is +CONSISTENCY_LOCAL_ONE+.
355
+ *
356
+ * @param consistency [Integer, Symbol] A consistency level.
357
+ * Symbols such as +:quorum+ or +:local_serial+ are also accepted.
358
+ * @return [Cassandra::Cluster] self.
359
+ * @raise [ArgumentError] If an invalid consistency level was given.
360
+ * @raise [RangeError] If the value does not fit in a C long.
361
+ */
362
+ static VALUE cluster_consistency(VALUE self, VALUE consistency)
363
+ {
364
+ CassandraCluster *cassandra_cluster;
365
+ CassConsistency consistency_value = cluster_value_to_consistency(consistency, "consistency");
366
+
367
+ GET_CLUSTER(self, cassandra_cluster);
368
+ cass_cluster_set_consistency(cassandra_cluster->cluster, consistency_value);
369
+
370
+ return self;
371
+ }
372
+
373
+ /**
374
+ * Sets the default serial consistency level of the statement.
375
+ * Default is +CONSISTENCY_ANY+.
376
+ *
377
+ * @param consistency [Integer, Symbol] A serial consistency level. Only
378
+ * +CONSISTENCY_SERIAL+/+CONSISTENCY_LOCAL_SERIAL+ (or +:serial+/+:local_serial+)
379
+ * are accepted.
380
+ * @return [Cassandra::Cluster] self.
381
+ * @raise [ArgumentError] If an invalid consistency level was given, or if it is
382
+ * not +CONSISTENCY_SERIAL+ or +CONSISTENCY_LOCAL_SERIAL+.
383
+ * @raise [RangeError] If the value does not fit in a C long.
384
+ */
385
+ static VALUE cluster_serial_consistency(VALUE self, VALUE consistency)
386
+ {
387
+ CassandraCluster *cassandra_cluster;
388
+ CassConsistency consistency_value = cluster_value_to_consistency(consistency, "serial_consistency");
389
+
390
+ // Cassandra only accepts SERIAL/LOCAL_SERIAL as a serial consistency
391
+ // level (used for lightweight transactions); any other value passes
392
+ // the generic range check above but is later rejected by the server
393
+ // at query time with an opaque error, so validate it here.
394
+ if (consistency_value != CASS_CONSISTENCY_SERIAL && consistency_value != CASS_CONSISTENCY_LOCAL_SERIAL) {
395
+ rb_raise(rb_eArgError, "Invalid serial_consistency: %"PRIsVALUE"", consistency);
396
+ }
397
+
398
+ GET_CLUSTER(self, cassandra_cluster);
399
+ cass_cluster_set_serial_consistency(cassandra_cluster->cluster, consistency_value);
400
+
401
+ return self;
402
+ }
403
+
404
+ /**
405
+ * Sets the number of IO threads that will handle query requests.
406
+ * Default is +1+.
407
+ *
408
+ * @param num_threads [Integer] A number of IO threads.
409
+ * @return [Cassandra::Cluster] self.
410
+ * @raise [ArgumentError] If zero was given.
411
+ * @raise [RangeError] If a negative value was given.
412
+ */
413
+ static VALUE cluster_num_threads_io(VALUE self, VALUE num_threads)
414
+ {
415
+ CassandraCluster *cassandra_cluster;
416
+
417
+ GET_CLUSTER(self, cassandra_cluster);
418
+ cluster_check_error(cass_cluster_set_num_threads_io(cassandra_cluster->cluster, cluster_value_to_uint(num_threads, "num_threads_io")), "num_threads_io");
419
+
420
+ return self;
421
+ }
422
+
423
+ /**
424
+ * Sets the size of the fixed size queue that stores pending requests.
425
+ * Default is +8192+.
426
+ *
427
+ * @param queue_size [Integer] A queue size.
428
+ * @return [Cassandra::Cluster] self.
429
+ * @raise [ArgumentError] If zero was given.
430
+ * @raise [RangeError] If a negative value was given.
431
+ */
432
+ static VALUE cluster_queue_size_io(VALUE self, VALUE queue_size)
433
+ {
434
+ CassandraCluster *cassandra_cluster;
435
+
436
+ GET_CLUSTER(self, cassandra_cluster);
437
+ cluster_check_error(cass_cluster_set_queue_size_io(cassandra_cluster->cluster, cluster_value_to_uint(queue_size, "queue_size_io")), "queue_size_io");
438
+
439
+ return self;
440
+ }
441
+
442
+ /**
443
+ * Sets the number of connections made to each server in each IO thread.
444
+ * Default is +1+.
445
+ *
446
+ * @param num_connections [Integer] A number of connections.
447
+ * @return [Cassandra::Cluster] self.
448
+ * @raise [ArgumentError] If zero was given.
449
+ * @raise [RangeError] If a negative value was given.
450
+ */
451
+ static VALUE cluster_core_connections_per_host(VALUE self, VALUE num_connections)
452
+ {
453
+ CassandraCluster *cassandra_cluster;
454
+
455
+ GET_CLUSTER(self, cassandra_cluster);
456
+ cluster_check_error(cass_cluster_set_core_connections_per_host(cassandra_cluster->cluster, cluster_value_to_uint(num_connections, "core_connections_per_host")), "core_connections_per_host");
457
+
458
+ return self;
459
+ }
460
+
461
+ /**
462
+ * Configures the cluster to use a reconnection policy that waits a constant
463
+ * time between each reconnection attempt.
464
+ * The exponential policy is used unless this method is called.
465
+ *
466
+ * @param delay_ms [Integer] A delay in milliseconds.
467
+ * @return [Cassandra::Cluster] self.
468
+ * @raise [RangeError] If a negative value was given.
469
+ */
470
+ static VALUE cluster_constant_reconnect(VALUE self, VALUE delay_ms)
471
+ {
472
+ CassandraCluster *cassandra_cluster;
473
+
474
+ GET_CLUSTER(self, cassandra_cluster);
475
+ cass_cluster_set_constant_reconnect(cassandra_cluster->cluster, cluster_value_to_uint64(delay_ms, "constant_reconnect"));
476
+
477
+ return self;
478
+ }
479
+
480
+ /**
481
+ * Configures the cluster to use a reconnection policy that waits
482
+ * exponentially longer between each reconnection attempt; however
483
+ * will maintain a constant delay once the maximum delay is reached.
484
+ * This is the default policy, with a base delay of +2000+ milliseconds and
485
+ * a maximum delay of +60000+ milliseconds.
486
+ *
487
+ * @param base_delay_ms [Integer] A base delay in milliseconds.
488
+ * @param max_delay_ms [Integer] A maximum delay in milliseconds.
489
+ * @return [Cassandra::Cluster] self.
490
+ * @raise [ArgumentError] If the base delay is 1 or less, the maximum delay is 1 or less,
491
+ * or the maximum delay is less than the base delay.
492
+ * @raise [RangeError] If a negative value was given.
493
+ */
494
+ static VALUE cluster_exponential_reconnect(VALUE self, VALUE base_delay_ms, VALUE max_delay_ms)
495
+ {
496
+ CassandraCluster *cassandra_cluster;
497
+
498
+ GET_CLUSTER(self, cassandra_cluster);
499
+ cluster_check_error(cass_cluster_set_exponential_reconnect(cassandra_cluster->cluster, cluster_value_to_uint64(base_delay_ms, "exponential_reconnect base_delay_ms"), cluster_value_to_uint64(max_delay_ms, "exponential_reconnect max_delay_ms")), "exponential_reconnect");
500
+
501
+ return self;
502
+ }
503
+
504
+ /**
505
+ * Enables/Disables Nagle's algorithm on the connections.
506
+ * Default is +true+ (disables Nagle's algorithm).
507
+ *
508
+ * @param enabled [Boolean] Whether to disable Nagle's algorithm.
509
+ * @return [Cassandra::Cluster] self.
510
+ */
511
+ static VALUE cluster_tcp_nodelay(VALUE self, VALUE enabled)
512
+ {
513
+ CassandraCluster *cassandra_cluster;
514
+
515
+ GET_CLUSTER(self, cassandra_cluster);
516
+ cass_cluster_set_tcp_nodelay(cassandra_cluster->cluster, RTEST(enabled) ? cass_true : cass_false);
517
+
518
+ return self;
519
+ }
520
+
521
+ /**
522
+ * Enables/Disables TCP keep-alive.
523
+ * Default is +false+ (disabled).
524
+ *
525
+ * @param enabled [Boolean] Whether to enable TCP keep-alive.
526
+ * @param delay_secs [Integer] The initial delay in seconds. Ignored when disabled.
527
+ * @return [Cassandra::Cluster] self.
528
+ * @raise [RangeError] If a negative value was given.
529
+ */
530
+ static VALUE cluster_tcp_keepalive(VALUE self, VALUE enabled, VALUE delay_secs)
531
+ {
532
+ CassandraCluster *cassandra_cluster;
533
+
534
+ GET_CLUSTER(self, cassandra_cluster);
535
+ cass_cluster_set_tcp_keepalive(cassandra_cluster->cluster, RTEST(enabled) ? cass_true : cass_false, cluster_value_to_uint(delay_secs, "tcp_keepalive delay_secs"));
536
+
537
+ return self;
538
+ }
539
+
540
+ /**
541
+ * Sets the amount of time between heartbeat messages and controls the amount
542
+ * of time the connection must be idle before sending heartbeat messages.
543
+ * This is useful for preventing intermediate network devices from dropping connections.
544
+ * Default is +30+ seconds.
545
+ *
546
+ * @param interval_secs [Integer] An interval in seconds. +0+ disables heartbeat messages.
547
+ * @return [Cassandra::Cluster] self.
548
+ * @raise [RangeError] If a negative value was given.
549
+ */
550
+ static VALUE cluster_connection_heartbeat_interval(VALUE self, VALUE interval_secs)
551
+ {
552
+ CassandraCluster *cassandra_cluster;
553
+
554
+ GET_CLUSTER(self, cassandra_cluster);
555
+ cass_cluster_set_connection_heartbeat_interval(cassandra_cluster->cluster, cluster_value_to_uint(interval_secs, "connection_heartbeat_interval"));
556
+
557
+ return self;
558
+ }
559
+
560
+ /**
561
+ * Sets the amount of time a connection is allowed to be without a successful
562
+ * heartbeat response before being terminated and scheduled for reconnection.
563
+ * Default is +60+ seconds.
564
+ *
565
+ * @param timeout_secs [Integer] A timeout in seconds.
566
+ * @return [Cassandra::Cluster] self.
567
+ * @raise [RangeError] If a negative value was given.
568
+ */
569
+ static VALUE cluster_connection_idle_timeout(VALUE self, VALUE timeout_secs)
570
+ {
571
+ CassandraCluster *cassandra_cluster;
572
+
573
+ GET_CLUSTER(self, cassandra_cluster);
574
+ cass_cluster_set_connection_idle_timeout(cassandra_cluster->cluster, cluster_value_to_uint(timeout_secs, "connection_idle_timeout"));
575
+
576
+ return self;
577
+ }
578
+
579
+ /**
580
+ * Configures the cluster to use round-robin load balancing.
581
+ * The driver discovers all nodes in a cluster and cycles through them per request.
582
+ *
583
+ * @return [Cassandra::Cluster] self.
584
+ */
585
+ static VALUE cluster_load_balance_round_robin(VALUE self)
586
+ {
587
+ CassandraCluster *cassandra_cluster;
588
+
589
+ GET_CLUSTER(self, cassandra_cluster);
590
+ cass_cluster_set_load_balance_round_robin(cassandra_cluster->cluster);
591
+
592
+ return self;
593
+ }
594
+
595
+ /**
596
+ * Configures the cluster to use DC-aware load balancing.
597
+ * For each query, all live nodes in a primary 'local' DC are tried first,
598
+ * followed by any node from other DCs.
599
+ *
600
+ * @param local_dc [String] The primary data center to try first.
601
+ * @return [Cassandra::Cluster] self.
602
+ * @raise [ArgumentError] If an invalid data center name was given.
603
+ */
604
+ static VALUE cluster_load_balance_dc_aware(VALUE self, VALUE local_dc)
605
+ {
606
+ CassandraCluster *cassandra_cluster;
607
+
608
+ GET_CLUSTER(self, cassandra_cluster);
609
+ // The 3rd and 4th parameters (used_hosts_per_remote_dc and
610
+ // allow_remote_dcs_for_local_cl) are deprecated in driver 2.16, so they
611
+ // are fixed to 0/cass_false and not exposed to Ruby.
612
+ cluster_check_error(cass_cluster_set_load_balance_dc_aware(cassandra_cluster->cluster, StringValueCStr(local_dc), 0, cass_false), "load_balance_dc_aware");
613
+
614
+ return self;
615
+ }
616
+
617
+ /**
618
+ * Configures the cluster to use token-aware request routing or not.
619
+ * Default is +true+ (enabled).
620
+ *
621
+ * @param enabled [Boolean] Whether to enable token-aware routing.
622
+ * @return [Cassandra::Cluster] self.
623
+ */
624
+ static VALUE cluster_token_aware_routing(VALUE self, VALUE enabled)
625
+ {
626
+ CassandraCluster *cassandra_cluster;
627
+
628
+ GET_CLUSTER(self, cassandra_cluster);
629
+ cass_cluster_set_token_aware_routing(cassandra_cluster->cluster, RTEST(enabled) ? cass_true : cass_false);
630
+
631
+ return self;
632
+ }
633
+
634
+ /**
635
+ * Configures the cluster to use latency-aware request routing or not.
636
+ * Default is +false+ (disabled).
637
+ *
638
+ * @param enabled [Boolean] Whether to enable latency-aware routing.
639
+ * @return [Cassandra::Cluster] self.
640
+ */
641
+ static VALUE cluster_latency_aware_routing(VALUE self, VALUE enabled)
642
+ {
643
+ CassandraCluster *cassandra_cluster;
644
+
645
+ GET_CLUSTER(self, cassandra_cluster);
646
+ cass_cluster_set_latency_aware_routing(cassandra_cluster->cluster, RTEST(enabled) ? cass_true : cass_false);
647
+
648
+ return self;
649
+ }
650
+
651
+ /**
652
+ * Enables/Disables retrieving and updating schema metadata. Disabling this
653
+ * can be useful to improve startup performance, but it means that
654
+ * 'Session#schema_metadata'-like features will not work.
655
+ * Default is +true+ (enabled).
656
+ *
657
+ * @param enabled [Boolean] Whether to keep schema metadata synchronized.
658
+ * @return [Cassandra::Cluster] self.
659
+ */
660
+ static VALUE cluster_use_schema(VALUE self, VALUE enabled)
661
+ {
662
+ CassandraCluster *cassandra_cluster;
663
+
664
+ GET_CLUSTER(self, cassandra_cluster);
665
+ cass_cluster_set_use_schema(cassandra_cluster->cluster, RTEST(enabled) ? cass_true : cass_false);
225
666
 
226
667
  return self;
227
668
  }
@@ -256,6 +697,10 @@ static void cluster_compact(void *ptr)
256
697
 
257
698
  void Init_cluster(void)
258
699
  {
700
+ for (size_t i = 0; i < sizeof(cluster_consistency_symbols) / sizeof(cluster_consistency_symbols[0]); i++) {
701
+ cluster_consistency_symbols[i].id = rb_intern(cluster_consistency_symbols[i].name);
702
+ }
703
+
259
704
  rb_define_alloc_func(cCluster, cluster_allocator);
260
705
  rb_define_method(cCluster, "initialize", cluster_initialize, 0);
261
706
  rb_define_method(cCluster, "connect", cluster_connect, 0);
@@ -267,6 +712,23 @@ void Init_cluster(void)
267
712
  rb_define_method(cCluster, "request_timeout", cluster_request_timeout, 1);
268
713
  rb_define_method(cCluster, "resolve_timeout", cluster_resolve_timeout, 1);
269
714
  rb_define_method(cCluster, "constant_speculative_execution_policy", cluster_constant_speculative_execution_policy, 2);
715
+ rb_define_method(cCluster, "credentials", cluster_credentials, 2);
716
+ rb_define_method(cCluster, "consistency", cluster_consistency, 1);
717
+ rb_define_method(cCluster, "serial_consistency", cluster_serial_consistency, 1);
718
+ rb_define_method(cCluster, "num_threads_io", cluster_num_threads_io, 1);
719
+ rb_define_method(cCluster, "queue_size_io", cluster_queue_size_io, 1);
720
+ rb_define_method(cCluster, "core_connections_per_host", cluster_core_connections_per_host, 1);
721
+ rb_define_method(cCluster, "constant_reconnect", cluster_constant_reconnect, 1);
722
+ rb_define_method(cCluster, "exponential_reconnect", cluster_exponential_reconnect, 2);
723
+ rb_define_method(cCluster, "tcp_nodelay", cluster_tcp_nodelay, 1);
724
+ rb_define_method(cCluster, "tcp_keepalive", cluster_tcp_keepalive, 2);
725
+ rb_define_method(cCluster, "connection_heartbeat_interval", cluster_connection_heartbeat_interval, 1);
726
+ rb_define_method(cCluster, "connection_idle_timeout", cluster_connection_idle_timeout, 1);
727
+ rb_define_method(cCluster, "load_balance_round_robin", cluster_load_balance_round_robin, 0);
728
+ rb_define_method(cCluster, "load_balance_dc_aware", cluster_load_balance_dc_aware, 1);
729
+ rb_define_method(cCluster, "token_aware_routing", cluster_token_aware_routing, 1);
730
+ rb_define_method(cCluster, "latency_aware_routing", cluster_latency_aware_routing, 1);
731
+ rb_define_method(cCluster, "use_schema", cluster_use_schema, 1);
270
732
 
271
733
  rb_define_const(cCluster, "PROTOCOL_VERSION_V1", INT2NUM(CASS_PROTOCOL_VERSION_V1));
272
734
  rb_define_const(cCluster, "PROTOCOL_VERSION_V2", INT2NUM(CASS_PROTOCOL_VERSION_V2));
@@ -275,4 +737,16 @@ void Init_cluster(void)
275
737
  rb_define_const(cCluster, "PROTOCOL_VERSION_V5", INT2NUM(CASS_PROTOCOL_VERSION_V5));
276
738
  rb_define_const(cCluster, "PROTOCOL_VERSION_DSEV1", INT2NUM(CASS_PROTOCOL_VERSION_DSEV1));
277
739
  rb_define_const(cCluster, "PROTOCOL_VERSION_DSEV2", INT2NUM(CASS_PROTOCOL_VERSION_DSEV2));
740
+
741
+ rb_define_const(cCluster, "CONSISTENCY_ANY", INT2NUM(CASS_CONSISTENCY_ANY));
742
+ rb_define_const(cCluster, "CONSISTENCY_ONE", INT2NUM(CASS_CONSISTENCY_ONE));
743
+ rb_define_const(cCluster, "CONSISTENCY_TWO", INT2NUM(CASS_CONSISTENCY_TWO));
744
+ rb_define_const(cCluster, "CONSISTENCY_THREE", INT2NUM(CASS_CONSISTENCY_THREE));
745
+ rb_define_const(cCluster, "CONSISTENCY_QUORUM", INT2NUM(CASS_CONSISTENCY_QUORUM));
746
+ rb_define_const(cCluster, "CONSISTENCY_ALL", INT2NUM(CASS_CONSISTENCY_ALL));
747
+ rb_define_const(cCluster, "CONSISTENCY_LOCAL_QUORUM", INT2NUM(CASS_CONSISTENCY_LOCAL_QUORUM));
748
+ rb_define_const(cCluster, "CONSISTENCY_EACH_QUORUM", INT2NUM(CASS_CONSISTENCY_EACH_QUORUM));
749
+ rb_define_const(cCluster, "CONSISTENCY_SERIAL", INT2NUM(CASS_CONSISTENCY_SERIAL));
750
+ rb_define_const(cCluster, "CONSISTENCY_LOCAL_SERIAL", INT2NUM(CASS_CONSISTENCY_LOCAL_SERIAL));
751
+ rb_define_const(cCluster, "CONSISTENCY_LOCAL_ONE", INT2NUM(CASS_CONSISTENCY_LOCAL_ONE));
278
752
  }
data/ext/ilios/future.c CHANGED
@@ -136,7 +136,13 @@ static void future_result_success_yield(CassandraFuture *cassandra_future)
136
136
  static void future_result_failure_yield(CassandraFuture *cassandra_future)
137
137
  {
138
138
  if (cassandra_future->on_failure_block) {
139
- rb_proc_call_with_block(cassandra_future->on_failure_block, 0, NULL, Qnil);
139
+ if (rb_proc_arity(cassandra_future->on_failure_block)) {
140
+ VALUE error = ilios_future_error_new(eExecutionError, NULL, cassandra_future->future);
141
+
142
+ rb_proc_call_with_block(cassandra_future->on_failure_block, 1, &error, Qnil);
143
+ } else {
144
+ rb_proc_call_with_block(cassandra_future->on_failure_block, 0, NULL, Qnil);
145
+ }
140
146
  }
141
147
  }
142
148
 
@@ -317,6 +323,9 @@ static VALUE future_on_failure_synchronize(VALUE future)
317
323
  /**
318
324
  * Run block when future resolves to error.
319
325
  *
326
+ * @yieldparam error [Cassandra::ExecutionError] The failure reason. Only
327
+ * yielded when the block accepts an argument; a zero-arity block is still
328
+ * called with no arguments.
320
329
  * @return [Cassandra::Future] self.
321
330
  * @raise [Cassandra::ExecutionError] If this method will be called twice.
322
331
  * @raise [ArgumentError] If no block was given.
data/ext/ilios/ilios.c CHANGED
@@ -1,5 +1,7 @@
1
1
  #include "ilios.h"
2
2
 
3
+ #include <string.h>
4
+
3
5
  VALUE mIlios;
4
6
  VALUE mCassandra;
5
7
  VALUE cCluster;
@@ -21,6 +23,7 @@ VALUE id_push;
21
23
  VALUE id_pop;
22
24
  VALUE id_alive;
23
25
  VALUE id_report_on_exception;
26
+ VALUE id_code;
24
27
  VALUE sym_unsupported_column_type;
25
28
 
26
29
  #if defined(HAVE_MALLOC_USABLE_SIZE)
@@ -74,6 +77,45 @@ static VALUE cassandra_set_log_level(VALUE self, VALUE log_level)
74
77
  return self;
75
78
  }
76
79
 
80
+ VALUE ilios_error_new(VALUE exception_class, VALUE message, CassError error_code)
81
+ {
82
+ VALUE error = rb_exc_new_str(exception_class, message);
83
+
84
+ rb_ivar_set(error, id_code, INT2NUM(error_code));
85
+
86
+ return error;
87
+ }
88
+
89
+ VALUE ilios_future_error_new(VALUE exception_class, const char *prefix, CassFuture *future)
90
+ {
91
+ CassError error_code;
92
+ const char *message;
93
+ size_t message_length;
94
+ VALUE body;
95
+ VALUE full_message;
96
+
97
+ error_code = cass_future_error_code(future);
98
+ cass_future_error_message(future, &message, &message_length);
99
+ if (message_length == 0) {
100
+ message = cass_error_desc(error_code);
101
+ message_length = strlen(message);
102
+ }
103
+
104
+ // The driver hands back the server's raw bytes, which rb_str_new would
105
+ // tag ASCII-8BIT; CQL identifiers and literals are UTF-8, so tag it
106
+ // UTF-8 and keep the prefixed message in that encoding too. The message
107
+ // can also carry driver-local text (strerror output, host names), which
108
+ // is not guaranteed UTF-8, so replace invalid bytes rather than handing
109
+ // back a string that raises ArgumentError on the first regexp match.
110
+ body = rb_utf8_str_new(message, message_length);
111
+ if (rb_enc_str_coderange(body) == ENC_CODERANGE_BROKEN) {
112
+ body = rb_str_scrub(body, Qnil);
113
+ }
114
+ full_message = prefix ? rb_enc_sprintf(rb_utf8_encoding(), "%s: %"PRIsVALUE, prefix, body) : body;
115
+
116
+ return ilios_error_new(exception_class, full_message, error_code);
117
+ }
118
+
77
119
  void Init_ilios(void)
78
120
  {
79
121
  rb_ext_ractor_safe(true);
@@ -88,6 +130,11 @@ void Init_ilios(void)
88
130
  eConnectError = rb_define_class_under(mCassandra, "ConnectError", rb_eStandardError);
89
131
  eExecutionError = rb_define_class_under(mCassandra, "ExecutionError", rb_eStandardError);
90
132
  eStatementError = rb_define_class_under(mCassandra, "StatementError", rb_eStandardError);
133
+ // @code is only set on errors built by ilios_error_new; the ones raised
134
+ // elsewhere via plain rb_raise leave #code nil.
135
+ rb_define_attr(eExecutionError, "code", 1, 0);
136
+ rb_define_attr(eConnectError, "code", 1, 0);
137
+ rb_define_attr(eStatementError, "code", 1, 0);
91
138
 
92
139
  cSizedQueue = rb_const_get(rb_cThread, rb_intern("SizedQueue"));
93
140
  rb_require("set");
@@ -101,6 +148,7 @@ void Init_ilios(void)
101
148
  id_pop = rb_intern("pop");
102
149
  id_alive = rb_intern("alive?");
103
150
  id_report_on_exception = rb_intern("report_on_exception=");
151
+ id_code = rb_intern("@code");
104
152
  sym_unsupported_column_type = ID2SYM(rb_intern("unsupported_column_type"));
105
153
 
106
154
  rb_define_module_function(mCassandra, "log_level", cassandra_set_log_level, 1);
data/ext/ilios/ilios.h CHANGED
@@ -11,7 +11,16 @@
11
11
 
12
12
  #define DEFAULT_PAGE_SIZE 10000
13
13
 
14
- #define GET_CLUSTER(obj, var) TypedData_Get_Struct(obj, CassandraCluster, &cassandra_cluster_data_type, var)
14
+ // Cluster exposes an allocator, so allocate and dup both produce an instance
15
+ // that never ran initialize. Reject it here instead of handing NULL to the
16
+ // driver, which segfaults.
17
+ #define GET_CLUSTER(obj, var) do { \
18
+ GET_UNINITIALIZED_CLUSTER(obj, var); \
19
+ if ((var)->cluster == NULL) { \
20
+ rb_raise(rb_eRuntimeError, "uninitialized Ilios::Cassandra::Cluster"); \
21
+ } \
22
+ } while (0)
23
+ #define GET_UNINITIALIZED_CLUSTER(obj, var) TypedData_Get_Struct(obj, CassandraCluster, &cassandra_cluster_data_type, var)
15
24
  #define GET_SESSION(obj, var) TypedData_Get_Struct(obj, CassandraSession, &cassandra_session_data_type, var)
16
25
  #define GET_STATEMENT(obj, var) TypedData_Get_Struct(obj, CassandraStatement, &cassandra_statement_data_type, var)
17
26
  #define GET_RESULT(obj, var) TypedData_Get_Struct(obj, CassandraResult, &cassandra_result_data_type, var)
@@ -118,6 +127,7 @@ extern VALUE id_push;
118
127
  extern VALUE id_pop;
119
128
  extern VALUE id_alive;
120
129
  extern VALUE id_report_on_exception;
130
+ extern VALUE id_code;
121
131
  extern VALUE sym_unsupported_column_type;
122
132
 
123
133
  extern void Init_cluster(void);
@@ -136,5 +146,17 @@ extern void statement_default_config(CassandraStatement *cassandra_statement);
136
146
  extern CassStatement *statement_build_for_execution(CassandraStatement *cassandra_statement);
137
147
  extern void result_await(CassandraResult *cassandra_result);
138
148
 
149
+ // Builds an exception of `exception_class` carrying `message` and `@code`
150
+ // (readable via #code) set to the CassError value.
151
+ extern VALUE ilios_error_new(VALUE exception_class, VALUE message, CassError error_code);
152
+
153
+ // Builds an exception of `exception_class` from `future`'s error: the
154
+ // driver's server-reported message (falling back to the generic
155
+ // cass_error_desc when the driver returns none), formatted as
156
+ // "<prefix>: <message>" when `prefix` is non-NULL, or the bare message when
157
+ // `prefix` is NULL. `future` must still be alive (not yet cass_future_free'd)
158
+ // when this is called.
159
+ extern VALUE ilios_future_error_new(VALUE exception_class, const char *prefix, CassFuture *future);
160
+
139
161
 
140
162
  #endif // ILIOS_H
data/ext/ilios/result.c CHANGED
@@ -22,10 +22,9 @@ void result_await(CassandraResult *cassandra_result)
22
22
  nogvl_future_wait(cassandra_result->future);
23
23
 
24
24
  if (cass_future_error_code(cassandra_result->future) != CASS_OK) {
25
- char error[4096] = { 0 };
25
+ VALUE error = ilios_future_error_new(eExecutionError, "Unable to wait executing", cassandra_result->future);
26
26
 
27
- strncpy(error, cass_error_desc(cass_future_error_code(cassandra_result->future)), sizeof(error) - 1);
28
- rb_raise(eExecutionError, "Unable to wait executing: %s", error);
27
+ rb_exc_raise(error);
29
28
  }
30
29
 
31
30
  if (cassandra_result->result == NULL) {
@@ -66,8 +65,10 @@ static VALUE result_next_page(VALUE self)
66
65
 
67
66
  error_code = cass_future_error_code(result_future);
68
67
  if (error_code != CASS_OK) {
68
+ VALUE error = ilios_future_error_new(eExecutionError, "Unable to wait executing", result_future);
69
+
69
70
  cass_future_free(result_future);
70
- rb_raise(eExecutionError, "Unable to wait executing: %s", cass_error_desc(error_code));
71
+ rb_exc_raise(error);
71
72
  }
72
73
 
73
74
  cass_result_free(cassandra_result->result);
@@ -80,10 +81,20 @@ static VALUE result_next_page(VALUE self)
80
81
  return self;
81
82
  }
82
83
 
84
+ // No CassFuture is involved here: this fires while decoding an already
85
+ // fetched row's column value.
86
+ static VALUE result_check_value_error_new(CassError error_code, VALUE key)
87
+ {
88
+ VALUE message = rb_enc_sprintf(rb_utf8_encoding(), "Unable to get value of %"PRIsVALUE" column: %s",
89
+ key, cass_error_desc(error_code));
90
+
91
+ return ilios_error_new(eExecutionError, message, error_code);
92
+ }
93
+
83
94
  static void result_check_value(CassError error_code, VALUE key)
84
95
  {
85
96
  if (error_code != CASS_OK) {
86
- rb_raise(eExecutionError, "Unable to get value of %"PRIsVALUE" column: %s", key, cass_error_desc(error_code));
97
+ rb_exc_raise(result_check_value_error_new(error_code, key));
87
98
  }
88
99
  }
89
100
 
data/ext/ilios/session.c CHANGED
@@ -56,11 +56,10 @@ static VALUE session_prepare(VALUE self, VALUE query)
56
56
  nogvl_future_wait(prepare_future);
57
57
 
58
58
  if (cass_future_error_code(prepare_future) != CASS_OK) {
59
- char error[4096] = { 0 };
59
+ VALUE error = ilios_future_error_new(eExecutionError, "Unable to prepare query", prepare_future);
60
60
 
61
- strncpy(error, cass_error_desc(cass_future_error_code(prepare_future)), sizeof(error) - 1);
62
61
  cass_future_free(prepare_future);
63
- rb_raise(eExecutionError, "Unable to prepare query: %s", error);
62
+ rb_exc_raise(error);
64
63
  }
65
64
 
66
65
  cassandra_statement_obj = CREATE_STATEMENT(cassandra_statement);
@@ -141,7 +141,10 @@ static void statement_bind_collection(statement_bind_target *target, const CassD
141
141
  static void statement_bind_check(CassError result, VALUE key)
142
142
  {
143
143
  if (result != CASS_OK) {
144
- rb_raise(eStatementError, "Failed to bind value of %"PRIsVALUE" column: %s", key, cass_error_desc(result));
144
+ VALUE message = rb_enc_sprintf(rb_utf8_encoding(), "Failed to bind value of %"PRIsVALUE" column: %s",
145
+ key, cass_error_desc(result));
146
+
147
+ rb_exc_raise(ilios_error_new(eStatementError, message, result));
145
148
  }
146
149
  }
147
150
 
@@ -428,7 +431,7 @@ static VALUE statement_snapshot_value(const CassDataType *data_type, VALUE value
428
431
 
429
432
  length = RARRAY_LEN(array);
430
433
  snapshot = rb_ary_new_capa(length);
431
- for (long i = 0; i < RARRAY_LEN(array); i++) {
434
+ for (long i = 0; i < length && i < RARRAY_LEN(array); i++) {
432
435
  rb_ary_push(snapshot, statement_snapshot_value(element_type, rb_ary_entry(array, i)));
433
436
  }
434
437
  return rb_ary_freeze(snapshot);
data/lib/ilios/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ilios
4
- VERSION = '1.1.1'
4
+ VERSION = '1.2.0'
5
5
  public_constant :VERSION
6
6
 
7
7
  CASSANDRA_CPP_DRIVER_VERSION = '2.17.1'
data/sig/ilios.rbs CHANGED
@@ -21,6 +21,18 @@ module Ilios
21
21
  PROTOCOL_VERSION_DSEV1: Integer
22
22
  PROTOCOL_VERSION_DSEV2: Integer
23
23
 
24
+ CONSISTENCY_ANY: Integer
25
+ CONSISTENCY_ONE: Integer
26
+ CONSISTENCY_TWO: Integer
27
+ CONSISTENCY_THREE: Integer
28
+ CONSISTENCY_QUORUM: Integer
29
+ CONSISTENCY_ALL: Integer
30
+ CONSISTENCY_LOCAL_QUORUM: Integer
31
+ CONSISTENCY_EACH_QUORUM: Integer
32
+ CONSISTENCY_SERIAL: Integer
33
+ CONSISTENCY_LOCAL_SERIAL: Integer
34
+ CONSISTENCY_LOCAL_ONE: Integer
35
+
24
36
  def connect: () -> Ilios::Cassandra::Session
25
37
  def hosts: (Array[String]) -> self
26
38
  def port: (Integer) -> self
@@ -30,6 +42,23 @@ module Ilios
30
42
  def request_timeout: (Integer) -> self
31
43
  def resolve_timeout: (Integer) -> self
32
44
  def constant_speculative_execution_policy: (Integer, Integer) -> self
45
+ def credentials: (String, String) -> self
46
+ def consistency: (Integer | Symbol) -> self
47
+ def serial_consistency: (Integer | Symbol) -> self
48
+ def num_threads_io: (Integer) -> self
49
+ def queue_size_io: (Integer) -> self
50
+ def core_connections_per_host: (Integer) -> self
51
+ def constant_reconnect: (Integer) -> self
52
+ def exponential_reconnect: (Integer, Integer) -> self
53
+ def tcp_nodelay: (bool) -> self
54
+ def tcp_keepalive: (bool, Integer) -> self
55
+ def connection_heartbeat_interval: (Integer) -> self
56
+ def connection_idle_timeout: (Integer) -> self
57
+ def load_balance_round_robin: () -> self
58
+ def load_balance_dc_aware: (String) -> self
59
+ def token_aware_routing: (bool) -> self
60
+ def latency_aware_routing: (bool) -> self
61
+ def use_schema: (bool) -> self
33
62
  end
34
63
 
35
64
  class Session
@@ -46,9 +75,21 @@ module Ilios
46
75
  def idempotent=: (bool) -> self
47
76
  end
48
77
 
78
+ class ExecutionError < StandardError
79
+ def code: () -> Integer?
80
+ end
81
+
82
+ class ConnectError < StandardError
83
+ def code: () -> Integer?
84
+ end
85
+
86
+ class StatementError < StandardError
87
+ def code: () -> Integer?
88
+ end
89
+
49
90
  class Future
50
- def on_success: () { (Ilios::Cassandra::Result) -> void } -> self
51
- def on_failure: () { () -> void } -> self
91
+ def on_success: () { (Ilios::Cassandra::Result | Ilios::Cassandra::Statement) -> void } -> self
92
+ def on_failure: () { (Ilios::Cassandra::ExecutionError) -> void } -> self
52
93
  def await: () -> self
53
94
  end
54
95
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ilios
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Watson
@@ -58,7 +58,7 @@ metadata:
58
58
  homepage_uri: https://github.com/Watson1978/ilios
59
59
  source_code_uri: https://github.com/Watson1978/ilios
60
60
  bug_tracker_uri: https://github.com/Watson1978/ilios/issues
61
- documentation_uri: https://www.rubydoc.info/gems/ilios/1.1.1
61
+ documentation_uri: https://www.rubydoc.info/gems/ilios/1.2.0
62
62
  rubygems_mfa_required: 'true'
63
63
  rdoc_options: []
64
64
  require_paths: