aerospike 2.29.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/README.md +13 -9
  4. data/lib/aerospike/batch_attr.rb +292 -0
  5. data/lib/aerospike/batch_delete.rb +48 -0
  6. data/lib/aerospike/batch_read.rb +97 -0
  7. data/lib/aerospike/batch_record.rb +83 -0
  8. data/lib/aerospike/batch_results.rb +38 -0
  9. data/lib/aerospike/batch_udf.rb +76 -0
  10. data/lib/aerospike/batch_write.rb +79 -0
  11. data/lib/aerospike/cdt/bit_operation.rb +4 -5
  12. data/lib/aerospike/cdt/map_operation.rb +24 -10
  13. data/lib/aerospike/cdt/map_policy.rb +6 -3
  14. data/lib/aerospike/cdt/map_return_type.rb +8 -0
  15. data/lib/aerospike/client.rb +39 -56
  16. data/lib/aerospike/cluster.rb +50 -46
  17. data/lib/aerospike/command/batch_index_command.rb +7 -11
  18. data/lib/aerospike/command/batch_index_node.rb +3 -4
  19. data/lib/aerospike/command/batch_operate_command.rb +151 -0
  20. data/lib/aerospike/command/batch_operate_node.rb +51 -0
  21. data/lib/aerospike/command/command.rb +231 -128
  22. data/lib/aerospike/exp/exp.rb +54 -27
  23. data/lib/aerospike/exp/exp_bit.rb +24 -24
  24. data/lib/aerospike/exp/exp_hll.rb +12 -12
  25. data/lib/aerospike/exp/exp_list.rb +101 -86
  26. data/lib/aerospike/exp/exp_map.rb +118 -110
  27. data/lib/aerospike/exp/operation.rb +2 -2
  28. data/lib/aerospike/info.rb +2 -4
  29. data/lib/aerospike/node.rb +20 -3
  30. data/lib/aerospike/operation.rb +38 -0
  31. data/lib/aerospike/policy/batch_delete_policy.rb +71 -0
  32. data/lib/aerospike/policy/batch_policy.rb +53 -4
  33. data/lib/aerospike/{command/batch_direct_node.rb → policy/batch_read_policy.rb} +17 -19
  34. data/lib/aerospike/policy/batch_udf_policy.rb +75 -0
  35. data/lib/aerospike/policy/batch_write_policy.rb +105 -0
  36. data/lib/aerospike/policy/policy.rb +3 -40
  37. data/lib/aerospike/query/query_command.rb +3 -205
  38. data/lib/aerospike/query/query_executor.rb +2 -2
  39. data/lib/aerospike/query/query_partition_command.rb +4 -230
  40. data/lib/aerospike/query/scan_executor.rb +2 -2
  41. data/lib/aerospike/query/scan_partition_command.rb +3 -3
  42. data/lib/aerospike/query/server_command.rb +2 -2
  43. data/lib/aerospike/query/statement.rb +5 -21
  44. data/lib/aerospike/task/execute_task.rb +2 -2
  45. data/lib/aerospike/utils/buffer.rb +15 -15
  46. data/lib/aerospike/version.rb +1 -1
  47. data/lib/aerospike.rb +13 -12
  48. metadata +16 -14
  49. data/lib/aerospike/command/batch_direct_command.rb +0 -105
  50. data/lib/aerospike/command/batch_direct_exists_command.rb +0 -51
  51. data/lib/aerospike/query/pred_exp/and_or.rb +0 -32
  52. data/lib/aerospike/query/pred_exp/geo_json_value.rb +0 -41
  53. data/lib/aerospike/query/pred_exp/integer_value.rb +0 -32
  54. data/lib/aerospike/query/pred_exp/op.rb +0 -27
  55. data/lib/aerospike/query/pred_exp/regex.rb +0 -32
  56. data/lib/aerospike/query/pred_exp/regex_flags.rb +0 -23
  57. data/lib/aerospike/query/pred_exp/string_value.rb +0 -29
  58. data/lib/aerospike/query/pred_exp.rb +0 -192
@@ -120,15 +120,15 @@ module Aerospike
120
120
  # Returns a node on the cluster for read operations
121
121
  def batch_read_node(partition, replica_policy)
122
122
  case replica_policy
123
- when Aerospike::Replica::MASTER, Aerospike::Replica::SEQUENCE
124
- return master_node(partition)
125
- when Aerospike::Replica::MASTER_PROLES
126
- return master_proles_node(partition)
127
- when Aerospike::Replica::PREFER_RACK
128
- return rack_node(partition, seq)
129
- when Aerospike::Replica::RANDOM
130
- return random_node
131
- else
123
+ when Aerospike::Replica::MASTER, Aerospike::Replica::SEQUENCE
124
+ master_node(partition)
125
+ when Aerospike::Replica::MASTER_PROLES
126
+ master_proles_node(partition)
127
+ when Aerospike::Replica::PREFER_RACK
128
+ rack_node(partition, seq)
129
+ when Aerospike::Replica::RANDOM
130
+ random_node
131
+ else
132
132
  raise Aerospike::Exceptions::InvalidNode("invalid policy.replica value")
133
133
  end
134
134
  end
@@ -136,17 +136,17 @@ module Aerospike
136
136
  # Returns a node on the cluster for read operations
137
137
  def read_node(partition, replica_policy, seq)
138
138
  case replica_policy
139
- when Aerospike::Replica::MASTER
140
- return master_node(partition)
141
- when Aerospike::Replica::MASTER_PROLES
142
- return master_proles_node(partition)
143
- when Aerospike::Replica::PREFER_RACK
144
- return rack_node(partition, seq)
145
- when Aerospike::Replica::SEQUENCE
146
- return sequence_node(partition, seq)
147
- when Aerospike::Replica::RANDOM
148
- return random_node
149
- else
139
+ when Aerospike::Replica::MASTER
140
+ master_node(partition)
141
+ when Aerospike::Replica::MASTER_PROLES
142
+ master_proles_node(partition)
143
+ when Aerospike::Replica::PREFER_RACK
144
+ rack_node(partition, seq)
145
+ when Aerospike::Replica::SEQUENCE
146
+ sequence_node(partition, seq)
147
+ when Aerospike::Replica::RANDOM
148
+ random_node
149
+ else
150
150
  raise Aerospike::Exceptions::InvalidNode("invalid policy.replica value")
151
151
  end
152
152
  end
@@ -155,12 +155,12 @@ module Aerospike
155
155
  def master_node(partition)
156
156
  partition_map = partitions
157
157
  replica_array = partition_map[partition.namespace]
158
- raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") if !replica_array
158
+ raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") unless replica_array
159
159
 
160
- node_array = (replica_array.get)[0]
161
- raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") if !node_array
160
+ node_array = replica_array.get[0]
161
+ raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") unless node_array
162
162
 
163
- node = (node_array.get)[partition.partition_id]
163
+ node = node_array.get[partition.partition_id]
164
164
  raise Aerospike::Exceptions::InvalidNode if !node || !node.active?
165
165
 
166
166
  node
@@ -170,7 +170,7 @@ module Aerospike
170
170
  def rack_node(partition, seq)
171
171
  partition_map = partitions
172
172
  replica_array = partition_map[partition.namespace]
173
- raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") if !replica_array
173
+ raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") unless replica_array
174
174
 
175
175
  replica_array = replica_array.get
176
176
 
@@ -179,10 +179,10 @@ module Aerospike
179
179
  node = nil
180
180
  fallback = nil
181
181
  for i in 1..replica_array.length
182
- idx = (seq.update{|v| v.succ} % replica_array.size).abs
183
- node = (replica_array[idx].get)[partition.partition_id]
182
+ idx = (seq.update { |v| v.succ } % replica_array.size).abs
183
+ node = replica_array[idx].get[partition.partition_id]
184
184
 
185
- next if !node
185
+ next unless node
186
186
 
187
187
  fallback = node
188
188
 
@@ -202,14 +202,14 @@ module Aerospike
202
202
  def master_proles_node(partition)
203
203
  partition_map = partitions
204
204
  replica_array = partition_map[partition.namespace]
205
- raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") if !replica_array
205
+ raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") unless replica_array
206
206
 
207
207
  replica_array = replica_array.get
208
208
 
209
209
  node = nil
210
210
  for replica in replica_array
211
- idx = (@replica_index.update{|v| v.succ} % replica_array.size).abs
212
- node = (replica_array[idx].get)[partition.partition_id]
211
+ idx = (@replica_index.update { |v| v.succ } % replica_array.size).abs
212
+ node = replica_array[idx].get[partition.partition_id]
213
213
 
214
214
  return node if node && node.active?
215
215
  end
@@ -221,14 +221,14 @@ module Aerospike
221
221
  def sequence_node(partition, seq)
222
222
  partition_map = partitions
223
223
  replica_array = partition_map[partition.namespace]
224
- raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") if !replica_array
224
+ raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") unless replica_array
225
225
 
226
226
  replica_array = replica_array.get
227
227
 
228
228
  node = nil
229
229
  for replica in replica_array
230
- idx = (seq.update{|v| v.succ} % replica_array.size).abs
231
- node = (replica_array[idx].get)[partition.partition_id]
230
+ idx = (seq.update { |v| v.succ } % replica_array.size).abs
231
+ node = replica_array[idx].get[partition.partition_id]
232
232
 
233
233
  return node if node && node.active?
234
234
  end
@@ -236,9 +236,13 @@ module Aerospike
236
236
  raise Aerospike::Exceptions::InvalidNode
237
237
  end
238
238
 
239
- def get_node_for_key(replica_policy, key)
239
+ def get_node_for_key(replica_policy, key, is_write: false)
240
240
  partition = Partition.new_by_key(key)
241
- batch_read_node(partition, replica_policy)
241
+ if is_write
242
+ master_node(partition)
243
+ else
244
+ batch_read_node(partition, replica_policy)
245
+ end
242
246
  end
243
247
 
244
248
  # Returns partitions pertaining to a node
@@ -247,10 +251,10 @@ module Aerospike
247
251
 
248
252
  partition_map = partitions
249
253
  replica_array = partition_map[namespace]
250
- raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") if !replica_array
254
+ raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") unless replica_array
251
255
 
252
- node_array = (replica_array.get)[0]
253
- raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") if !node_array
256
+ node_array = replica_array.get[0]
257
+ raise Aerospike::Exceptions::InvalidNamespace("namespace not found in the partition map") unless node_array
254
258
 
255
259
 
256
260
  pid = 0
@@ -270,7 +274,7 @@ module Aerospike
270
274
  i = 0
271
275
  while i < length
272
276
  # Must handle concurrency with other non-tending threads, so node_index is consistent.
273
- idx = (@node_index.update{ |v| v.succ } % node_array.length).abs
277
+ idx = (@node_index.update { |v| v.succ } % node_array.length).abs
274
278
  node = node_array[idx]
275
279
 
276
280
  return node if node.active?
@@ -366,13 +370,13 @@ module Aerospike
366
370
  @tend_thread = Thread.new do
367
371
  Thread.current.abort_on_exception = false
368
372
  loop do
369
- begin
373
+
370
374
  tend
371
375
  sleep(@tend_interval / 1000.0)
372
- rescue => e
376
+ rescue => e
373
377
  Aerospike.logger.error("Exception occured during tend: #{e}")
374
378
  Aerospike.logger.debug { e.backtrace.join("\n") }
375
- end
379
+
376
380
  end
377
381
  end
378
382
  end
@@ -453,7 +457,7 @@ module Aerospike
453
457
 
454
458
  def log_tend_stats(nodes)
455
459
  diff = nodes.size - @old_node_count
456
- action = "#{diff.abs} #{diff.abs == 1 ? "node has" : "nodes have"} #{diff > 0 ? "joined" : "left"} the cluster."
460
+ action = "#{diff.abs} #{diff.abs == 1 ? 'node has' : 'nodes have'} #{diff > 0 ? 'joined' : 'left'} the cluster."
457
461
  Aerospike.logger.info("Tend finished. #{action} Old node count: #{@old_node_count}, New node count: #{nodes.size}")
458
462
  @old_node_count = nodes.size
459
463
  end
@@ -689,11 +693,11 @@ module Aerospike
689
693
  end
690
694
 
691
695
  def node_exists(search, node_list)
692
- node_list.any? {|node| node == search }
696
+ node_list.any? { |node| node == search }
693
697
  end
694
698
 
695
699
  def find_node_by_name(node_name)
696
- nodes.detect{|node| node.name == node_name }
700
+ nodes.detect { |node| node.name == node_name }
697
701
  end
698
702
  end
699
703
  end
@@ -21,11 +21,7 @@ module Aerospike
21
21
 
22
22
  class BatchIndexCommand < MultiCommand #:nodoc:
23
23
 
24
- attr_accessor :batch
25
- attr_accessor :policy
26
- attr_accessor :bin_names
27
- attr_accessor :results
28
- attr_accessor :read_attr
24
+ attr_accessor :batch, :policy, :bin_names, :results, :read_attr
29
25
 
30
26
  def initialize(node, batch, policy, bin_names, results, read_attr)
31
27
  super(node)
@@ -42,8 +38,8 @@ module Aerospike
42
38
  field_count_row = 1
43
39
  field_count = 1
44
40
 
45
- predexp_size = estimate_predexp(@policy.predexp)
46
- field_count += 1 if predexp_size > 0
41
+ exp_size = estimate_expression_size(@policy.filter_exp)
42
+ field_count += 1 if exp_size > 0
47
43
 
48
44
  if bin_names
49
45
  bin_names.each do |bin_name|
@@ -58,7 +54,7 @@ module Aerospike
58
54
  batch.keys.each do |key|
59
55
  @data_offset += key.digest.length + 4 # 4 byte batch offset
60
56
 
61
- if prev != nil && prev.namespace == key.namespace
57
+ if !prev.nil? && prev.namespace == key.namespace
62
58
  @data_offset += 1
63
59
  else
64
60
  @data_offset += key.namespace.bytesize + FIELD_HEADER_SIZE + 1 + 1 + 2 + 2 # repeat/no-repeat flag + read_attr flags + field_count + operation_count
@@ -66,9 +62,9 @@ module Aerospike
66
62
  end
67
63
  end
68
64
  size_buffer
69
- write_header(policy,read_attr | INFO1_BATCH, 0, field_count, 0)
65
+ write_header_read(policy, read_attr | INFO1_BATCH, 0, field_count, 0)
70
66
 
71
- write_predexp(@policy.predexp, predexp_size)
67
+ write_filter_exp(@policy.filter_exp, exp_size)
72
68
 
73
69
  write_field_header(0, Aerospike::FieldType::BATCH_INDEX)
74
70
  @data_offset += @data_buffer.write_int32(batch.keys.length, @data_offset)
@@ -80,7 +76,7 @@ module Aerospike
80
76
  @data_offset += @data_buffer.write_int32(index, @data_offset)
81
77
  @data_offset += @data_buffer.write_binary(key.digest, @data_offset)
82
78
 
83
- if (prev != nil && prev.namespace == key.namespace)
79
+ if !prev.nil? && prev.namespace == key.namespace
84
80
  @data_offset += @data_buffer.write_byte(1, @data_offset)
85
81
  else
86
82
  @data_offset += @data_buffer.write_byte(0, @data_offset)
@@ -19,13 +19,12 @@ module Aerospike
19
19
 
20
20
  class BatchIndexNode #:nodoc:
21
21
 
22
- attr_accessor :node
23
- attr_accessor :keys_by_idx
22
+ attr_accessor :node, :keys_by_idx
24
23
 
25
24
  def self.generate_list(cluster, replica_policy, keys)
26
25
  keys.each_with_index
27
- .group_by { |key, _| cluster.get_node_for_key(replica_policy, key) }
28
- .map { |node, keys_with_idx| BatchIndexNode.new(node, keys_with_idx) }
26
+ .group_by { |key, _| cluster.get_node_for_key(replica_policy, key) }
27
+ .map { |node, keys_with_idx| BatchIndexNode.new(node, keys_with_idx) }
29
28
  end
30
29
 
31
30
  def initialize(node, keys_with_idx)
@@ -0,0 +1,151 @@
1
+ # Copyright 2018 Aerospike, Inc.
2
+ #
3
+ # Portions may be licensed to Aerospike, Inc. under one or more contributor
4
+ # license agreements.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may not
7
+ # use this file except in compliance with the License. You may obtain a copy of
8
+ # the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
+ # License for the specific language governing permissions and limitations under
16
+ # the License.
17
+
18
+ require 'aerospike/command/multi_command'
19
+
20
+ module Aerospike
21
+
22
+ class BatchOperateCommand < MultiCommand #:nodoc:
23
+
24
+ attr_accessor :batch, :policy, :attr, :records
25
+
26
+ def initialize(node, batch, policy, records)
27
+ super(node)
28
+ @batch = batch
29
+ @policy = policy
30
+ @records = records
31
+ end
32
+
33
+ def batch_flags
34
+ flags = 0
35
+ # flags |= 0x1 if @policy.allow_inline
36
+ flags |= 0x2 if @policy.allow_inline_ssd
37
+ flags |= 0x4 if @policy.respond_all_keys
38
+ flags
39
+ end
40
+
41
+ def write_buffer
42
+ field_count = 1
43
+
44
+ exp_size = estimate_expression_size(@policy.filter_exp)
45
+ @data_offset += exp_size
46
+ field_count += 1 if exp_size > 0
47
+
48
+ @data_buffer.reset
49
+ begin_cmd
50
+ @data_offset += FIELD_HEADER_SIZE + 4 + 1 # batch.keys.length + flags
51
+
52
+ prev = nil
53
+ @records.each do |record|
54
+ key = record.key
55
+ @data_offset += key.digest.length + 4 # 4 byte batch offset
56
+
57
+ if !@policy.send_key && !prev.nil? && prev.key.namespace == key.namespace && prev.key.set_name == key.set_name && record == prev
58
+ @data_offset += 1
59
+ else
60
+ @data_offset += 12
61
+ @data_offset += key.namespace.bytesize + FIELD_HEADER_SIZE
62
+ @data_offset += key.set_name.bytesize + FIELD_HEADER_SIZE
63
+ @data_offset += record.size
64
+ end
65
+
66
+ prev = record
67
+ end
68
+ size_buffer
69
+ write_batch_header(policy, field_count)
70
+
71
+ write_filter_exp(@policy.filter_exp, exp_size)
72
+
73
+ field_size_offset = @data_offset
74
+
75
+ write_field_header(0, Aerospike::FieldType::BATCH_INDEX)
76
+ @data_offset += @data_buffer.write_int32(batch.records.length, @data_offset)
77
+ @data_offset += @data_buffer.write_byte(batch_flags, @data_offset)
78
+
79
+ prev = nil
80
+ attr = BatchAttr.new
81
+ batch.records.each_with_index do |record, index|
82
+ @data_offset += @data_buffer.write_int32(index, @data_offset)
83
+ key = record.key
84
+ @data_offset += @data_buffer.write_binary(key.digest, @data_offset)
85
+
86
+ if !@policy.send_key && !prev.nil? && prev.key.namespace == key.namespace && prev.key.set_name == key.set_name && record == prev
87
+ @data_offset += @data_buffer.write_byte(BATCH_MSG_REPEAT, @data_offset)
88
+ else
89
+ case record
90
+ when BatchRead
91
+ attr.set_batch_read(record.policy)
92
+ if record.bin_names&.length&.> 0
93
+ write_batch_bin_names(key, record.bin_names, attr, attr.filter_exp)
94
+ elsif record.ops&.length&.> 0
95
+ attr.adjust_read(br.ops)
96
+ write_batch_operations(key, record.ops, attr, attr.filter_exp)
97
+ else
98
+ attr.adjust_read_all_bins(record.read_all_bins)
99
+ write_batch_read(key, attr, attr.filter_exp, 0)
100
+ end
101
+
102
+ when BatchWrite
103
+ attr.set_batch_write(record.policy)
104
+ attr.adjust_write(record.ops)
105
+ write_batch_operations(key, record.ops, attr, attr.filter_exp)
106
+
107
+ when BatchUDF
108
+ attr.set_batch_udf(record.policy)
109
+ write_batch_write(key, attr, attr.filter_exp, 3, 0)
110
+ write_field_string(record.package_name, Aerospike::FieldType::UDF_PACKAGE_NAME)
111
+ write_field_string(record.function_name, Aerospike::FieldType::UDF_FUNCTION)
112
+ write_field_bytes(record.arg_bytes, Aerospike::FieldType::UDF_ARGLIST)
113
+
114
+ when BatchDelete
115
+ attr.set_batch_delete(record.policy)
116
+ write_batch_write(key, attr, attr.filter_exp, 0, 0)
117
+ end
118
+
119
+ prev = record
120
+ end
121
+ end
122
+
123
+ @data_buffer.write_uint32(@data_offset-MSG_TOTAL_HEADER_SIZE-4, field_size_offset)
124
+
125
+ end_cmd
126
+ mark_compressed(@policy)
127
+ end
128
+
129
+ # Parse all results in the batch. Add records to shared list.
130
+ # If the record was not found, the bins will be nil.
131
+ def parse_row(result_code)
132
+ generation = @data_buffer.read_int32(6)
133
+ expiration = @data_buffer.read_int32(10)
134
+ batch_index = @data_buffer.read_int32(14)
135
+ field_count = @data_buffer.read_int16(18)
136
+ op_count = @data_buffer.read_int16(20)
137
+
138
+ skip_key(field_count)
139
+ req_key = records[batch_index].key
140
+
141
+ records[batch_index].result_code = result_code
142
+ case result_code
143
+ when 0, ResultCode::UDF_BAD_RESPONSE
144
+ record = parse_record(req_key, op_count, generation, expiration)
145
+ records[batch_index].record = record
146
+ end
147
+ end
148
+
149
+ end # class
150
+
151
+ end # module
@@ -0,0 +1,51 @@
1
+ # Copyright 2018 Aerospike, Inc.
2
+ #
3
+ # Portions may be licensed to Aerospike, Inc. under one or more contributor
4
+ # license agreements.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may not
7
+ # use this file except in compliance with the License. You may obtain a copy of
8
+ # the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
+ # License for the specific language governing permissions and limitations under
16
+ # the License.
17
+
18
+ module Aerospike
19
+
20
+ class BatchOperateNode #:nodoc:
21
+
22
+ attr_accessor :node, :records_by_idx
23
+
24
+ def self.generate_list(cluster, replica_policy, records)
25
+ records.each_with_index
26
+ .group_by { |record, _| cluster.get_node_for_key(replica_policy, record.key, is_write: record.has_write) }
27
+ .map { |node, records_with_idx| BatchOperateNode.new(node, records_with_idx) }
28
+ end
29
+
30
+ def initialize(node, records_with_idx)
31
+ @node = node
32
+ @records_by_idx = records_with_idx.map(&:reverse).to_h
33
+ end
34
+
35
+ def records
36
+ records_by_idx.values
37
+ end
38
+
39
+ def each_record_with_index
40
+ records_by_idx.each do |idx, rec|
41
+ yield rec, idx
42
+ end
43
+ end
44
+
45
+ def record_for_index(idx)
46
+ @records_by_idx[idx]
47
+ end
48
+
49
+ end
50
+
51
+ end