aerospike 0.1.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 +7 -0
- data/CHANGELOG.md +0 -0
- data/LICENSE +203 -0
- data/README.md +123 -0
- data/lib/aerospike.rb +69 -0
- data/lib/aerospike/aerospike_exception.rb +111 -0
- data/lib/aerospike/bin.rb +46 -0
- data/lib/aerospike/client.rb +649 -0
- data/lib/aerospike/cluster/cluster.rb +537 -0
- data/lib/aerospike/cluster/connection.rb +113 -0
- data/lib/aerospike/cluster/node.rb +248 -0
- data/lib/aerospike/cluster/node_validator.rb +85 -0
- data/lib/aerospike/cluster/partition.rb +54 -0
- data/lib/aerospike/cluster/partition_tokenizer_new.rb +128 -0
- data/lib/aerospike/cluster/partition_tokenizer_old.rb +135 -0
- data/lib/aerospike/command/batch_command.rb +120 -0
- data/lib/aerospike/command/batch_command_exists.rb +93 -0
- data/lib/aerospike/command/batch_command_get.rb +150 -0
- data/lib/aerospike/command/batch_item.rb +69 -0
- data/lib/aerospike/command/batch_node.rb +82 -0
- data/lib/aerospike/command/command.rb +680 -0
- data/lib/aerospike/command/delete_command.rb +57 -0
- data/lib/aerospike/command/execute_command.rb +42 -0
- data/lib/aerospike/command/exists_command.rb +57 -0
- data/lib/aerospike/command/field_type.rb +44 -0
- data/lib/aerospike/command/operate_command.rb +37 -0
- data/lib/aerospike/command/read_command.rb +174 -0
- data/lib/aerospike/command/read_header_command.rb +63 -0
- data/lib/aerospike/command/single_command.rb +60 -0
- data/lib/aerospike/command/touch_command.rb +50 -0
- data/lib/aerospike/command/write_command.rb +60 -0
- data/lib/aerospike/host.rb +43 -0
- data/lib/aerospike/info.rb +96 -0
- data/lib/aerospike/key.rb +99 -0
- data/lib/aerospike/language.rb +25 -0
- data/lib/aerospike/ldt/large.rb +69 -0
- data/lib/aerospike/ldt/large_list.rb +100 -0
- data/lib/aerospike/ldt/large_map.rb +82 -0
- data/lib/aerospike/ldt/large_set.rb +78 -0
- data/lib/aerospike/ldt/large_stack.rb +72 -0
- data/lib/aerospike/loggable.rb +55 -0
- data/lib/aerospike/operation.rb +70 -0
- data/lib/aerospike/policy/client_policy.rb +37 -0
- data/lib/aerospike/policy/generation_policy.rb +37 -0
- data/lib/aerospike/policy/policy.rb +54 -0
- data/lib/aerospike/policy/priority.rb +34 -0
- data/lib/aerospike/policy/record_exists_action.rb +45 -0
- data/lib/aerospike/policy/write_policy.rb +61 -0
- data/lib/aerospike/record.rb +42 -0
- data/lib/aerospike/result_code.rb +353 -0
- data/lib/aerospike/task/index_task.rb +59 -0
- data/lib/aerospike/task/task.rb +71 -0
- data/lib/aerospike/task/udf_register_task.rb +55 -0
- data/lib/aerospike/task/udf_remove_task.rb +55 -0
- data/lib/aerospike/udf.rb +24 -0
- data/lib/aerospike/utils/buffer.rb +139 -0
- data/lib/aerospike/utils/epoc.rb +28 -0
- data/lib/aerospike/utils/pool.rb +65 -0
- data/lib/aerospike/value/particle_type.rb +45 -0
- data/lib/aerospike/value/value.rb +380 -0
- data/lib/aerospike/version.rb +4 -0
- metadata +132 -0
@@ -0,0 +1,537 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# Copyright 2014 Aerospike, Inc.
|
3
|
+
#
|
4
|
+
# Portions may be licensed to Aerospike, Inc. under one or more contributor
|
5
|
+
# license agreements.
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
8
|
+
# use this file except in compliance with the License. You may obtain a copy of
|
9
|
+
# the License at http:#www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
13
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
14
|
+
# License for the specific language governing permissions and limitations under
|
15
|
+
# the License.
|
16
|
+
|
17
|
+
require 'thread'
|
18
|
+
require 'timeout'
|
19
|
+
|
20
|
+
require 'atomic'
|
21
|
+
|
22
|
+
module Aerospike
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
class Cluster
|
27
|
+
|
28
|
+
attr_reader :connection_timeout, :connection_queue_size
|
29
|
+
|
30
|
+
def initialize(policy, *hosts)
|
31
|
+
@cluster_seeds = hosts
|
32
|
+
@connection_queue_size = policy.connection_queue_size
|
33
|
+
@connection_timeout = policy.timeout
|
34
|
+
@aliases = {}
|
35
|
+
@cluster_nodes = []
|
36
|
+
@partition_write_map = {}
|
37
|
+
@node_index = Atomic.new(0)
|
38
|
+
@closed = Atomic.new(false)
|
39
|
+
@mutex = Mutex.new
|
40
|
+
|
41
|
+
wait_till_stablized
|
42
|
+
|
43
|
+
if policy.fail_if_not_connected && !connected?
|
44
|
+
raise Aerospike::Exceptions::Aerospike.new(Aerospike::ResultCode::SERVER_NOT_AVAILABLE)
|
45
|
+
end
|
46
|
+
|
47
|
+
launch_tend_thread
|
48
|
+
|
49
|
+
Aerospike.logger.info('New cluster initialized and ready to be used...')
|
50
|
+
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_seeds(hosts)
|
55
|
+
@mutex.synchronize do
|
56
|
+
@cluster_seeds.concat(hosts)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def seeds
|
61
|
+
@mutex.synchronize do
|
62
|
+
@cluster_seeds.dup
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def connected?
|
67
|
+
# Must copy array reference for copy on write semantics to work.
|
68
|
+
node_array = nodes
|
69
|
+
(node_array.length > 0) && !@closed.value
|
70
|
+
end
|
71
|
+
|
72
|
+
def get_node(partition)
|
73
|
+
# Must copy hashmap reference for copy on write semantics to work.
|
74
|
+
nmap = partitions
|
75
|
+
if node_array = nmap[partition.namespace]
|
76
|
+
node = node_array.value[partition.partition_id]
|
77
|
+
|
78
|
+
if node && node.active?
|
79
|
+
return node
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
return random_node
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns a random node on the cluster
|
87
|
+
def random_node
|
88
|
+
# Must copy array reference for copy on write semantics to work.
|
89
|
+
node_array = nodes
|
90
|
+
length = node_array.length
|
91
|
+
for i in 0..length
|
92
|
+
# Must handle concurrency with other non-tending threads, so node_index is consistent.
|
93
|
+
index = (@node_index.update{|v| v+1} % node_array.length).abs
|
94
|
+
node = node_array[index]
|
95
|
+
|
96
|
+
if node.active?
|
97
|
+
return node
|
98
|
+
end
|
99
|
+
end
|
100
|
+
raise Aerospike::Exceptions::InvalidNode.new
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns a list of all nodes in the cluster
|
104
|
+
def nodes
|
105
|
+
@mutex.synchronize do
|
106
|
+
# Must copy array reference for copy on write semantics to work.
|
107
|
+
@cluster_nodes.dup
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Find a node by name and returns an error if not found
|
112
|
+
def get_node_by_name(node_name)
|
113
|
+
node = find_node_by_name(node_name)
|
114
|
+
|
115
|
+
raise Aerospike::Exceptions::InvalidNode.new unless node
|
116
|
+
|
117
|
+
node
|
118
|
+
end
|
119
|
+
|
120
|
+
# Closes all cached connections to the cluster nodes and stops the tend thread
|
121
|
+
def close
|
122
|
+
unless @closed.value
|
123
|
+
# send close signal to maintenance channel
|
124
|
+
@closed.value = true
|
125
|
+
@tend_thread.kill
|
126
|
+
|
127
|
+
nodes.each do |node|
|
128
|
+
node.close
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
def find_alias(aliass)
|
135
|
+
@mutex.synchronize do
|
136
|
+
@aliases[aliass]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def update_partitions(conn, node)
|
141
|
+
# TODO: Cluster should not care about version of tokenizer
|
142
|
+
# decouple clstr interface
|
143
|
+
nmap = {}
|
144
|
+
if node.use_new_info?
|
145
|
+
Aerospike.logger.info("Updating partitions using new protocol...")
|
146
|
+
|
147
|
+
tokens = PartitionTokenizerNew.new(conn)
|
148
|
+
nmap = tokens.update_partition(partitions, node)
|
149
|
+
else
|
150
|
+
Aerospike.logger.info("Updating partitions using old protocol...")
|
151
|
+
tokens = PartitionTokenizerOld.new(conn)
|
152
|
+
nmap = tokens.update_partition(partitions, node)
|
153
|
+
end
|
154
|
+
|
155
|
+
# update partition write map
|
156
|
+
set_partitions(nmap) if nmap
|
157
|
+
|
158
|
+
Aerospike.logger.info("Partitions updated...")
|
159
|
+
end
|
160
|
+
|
161
|
+
def request_info(policy, *commands)
|
162
|
+
node = random_node
|
163
|
+
conn = node.get_connection(policy.timeout)
|
164
|
+
Info.request(conn, *commands).tap do
|
165
|
+
node.put_connection(conn)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
def launch_tend_thread
|
172
|
+
@tend_thread = Thread.new do
|
173
|
+
abort_on_exception = false
|
174
|
+
while true
|
175
|
+
begin
|
176
|
+
tend
|
177
|
+
sleep 1 # 1 second
|
178
|
+
rescue => e
|
179
|
+
Aerospike.logger.error("Exception occured during tend: #{e}")
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def tend
|
186
|
+
nodes = self.nodes
|
187
|
+
|
188
|
+
# All node additions/deletions are performed in tend thread.
|
189
|
+
# If active nodes don't exist, seed cluster.
|
190
|
+
if nodes.empty?
|
191
|
+
Aerospike.logger.info("No connections available; seeding...")
|
192
|
+
seed_nodes
|
193
|
+
|
194
|
+
# refresh nodes list after seeding
|
195
|
+
nodes = self.nodes
|
196
|
+
end
|
197
|
+
|
198
|
+
# Refresh all known nodes.
|
199
|
+
friend_list = []
|
200
|
+
refresh_count = 0
|
201
|
+
|
202
|
+
# Clear node reference counts.
|
203
|
+
nodes.each do |node|
|
204
|
+
node.reference_count.value = 0
|
205
|
+
node.responded.value = false
|
206
|
+
|
207
|
+
if node.active?
|
208
|
+
begin
|
209
|
+
friends = node.refresh
|
210
|
+
refresh_count += 1
|
211
|
+
friend_list.concat(friends) if friends
|
212
|
+
rescue => e
|
213
|
+
Aerospike.logger.warn("Node `#{node}` refresh failed: #{e.to_s}")
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# Add nodes in a batch.
|
219
|
+
add_list = find_nodes_to_add(friend_list)
|
220
|
+
add_nodes(add_list) unless add_list.empty?
|
221
|
+
|
222
|
+
# Handle nodes changes determined from refreshes.
|
223
|
+
# Remove nodes in a batch.
|
224
|
+
remove_list = find_nodes_to_remove(refresh_count)
|
225
|
+
remove_nodes(remove_list) unless remove_list.empty?
|
226
|
+
|
227
|
+
Aerospike.logger.info("Tend finished. Live node count: #{nodes.length}")
|
228
|
+
end
|
229
|
+
|
230
|
+
def wait_till_stablized
|
231
|
+
count = -1
|
232
|
+
|
233
|
+
# will run until the cluster is stablized
|
234
|
+
thr = Thread.new do
|
235
|
+
abort_on_exception=true
|
236
|
+
while true
|
237
|
+
tend
|
238
|
+
|
239
|
+
# Check to see if cluster has changed since the last Tend.
|
240
|
+
# If not, assume cluster has stabilized and return.
|
241
|
+
if count == nodes.length
|
242
|
+
break
|
243
|
+
end
|
244
|
+
|
245
|
+
sleep(0.001) # sleep for a miliseconds
|
246
|
+
|
247
|
+
count = nodes.length
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# wait for the thread to finish or timeout
|
252
|
+
begin
|
253
|
+
Timeout.timeout(1) do
|
254
|
+
thr.join
|
255
|
+
end
|
256
|
+
rescue Timeout::Error
|
257
|
+
thr.kill if thr.alive?
|
258
|
+
end
|
259
|
+
|
260
|
+
end
|
261
|
+
|
262
|
+
def set_partitions(part_map)
|
263
|
+
@mutex.synchronize do
|
264
|
+
@partition_write_map = part_map
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def partitions
|
269
|
+
res = nil
|
270
|
+
@mutex.synchronize do
|
271
|
+
res = @partition_write_map
|
272
|
+
end
|
273
|
+
|
274
|
+
res
|
275
|
+
end
|
276
|
+
|
277
|
+
def seed_nodes
|
278
|
+
seed_array = seeds
|
279
|
+
|
280
|
+
Aerospike.logger.info("Seeding the cluster. Seeds count: #{seed_array.length}")
|
281
|
+
|
282
|
+
list = []
|
283
|
+
|
284
|
+
seed_array.each do |seed|
|
285
|
+
begin
|
286
|
+
seed_node_validator = NodeValidator.new(seed, @connection_timeout)
|
287
|
+
rescue => e
|
288
|
+
Aerospike.logger.warn("Seed #{seed.to_s} failed: #{e}")
|
289
|
+
next
|
290
|
+
end
|
291
|
+
|
292
|
+
nv = nil
|
293
|
+
# Seed host may have multiple aliases in the case of round-robin dns configurations.
|
294
|
+
seed_node_validator.aliases.each do |aliass|
|
295
|
+
|
296
|
+
if aliass == seed
|
297
|
+
nv = seed_node_validator
|
298
|
+
else
|
299
|
+
begin
|
300
|
+
nv = NodeValidator.new(aliass, @connection_timeout)
|
301
|
+
rescue Exection => e
|
302
|
+
Aerospike.logger.warn("Seed #{seed.to_s} failed: #{e}")
|
303
|
+
next
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
if !find_node_name(list, nv.name)
|
308
|
+
node = create_node(nv)
|
309
|
+
add_aliases(node)
|
310
|
+
list << node
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
end
|
315
|
+
|
316
|
+
if list.length > 0
|
317
|
+
add_nodes_copy(list)
|
318
|
+
end
|
319
|
+
|
320
|
+
end
|
321
|
+
|
322
|
+
# Finds a node by name in a list of nodes
|
323
|
+
def find_node_name(list, name)
|
324
|
+
list.any?{|name| node.name == name}
|
325
|
+
end
|
326
|
+
|
327
|
+
def add_alias(host, node)
|
328
|
+
if host && node
|
329
|
+
@mutex.synchronize do
|
330
|
+
@aliases[host] = node
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
def remove_alias(aliass)
|
336
|
+
if aliass
|
337
|
+
@mutex.synchronize do
|
338
|
+
@aliases.delete(aliass)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
def find_nodes_to_add(hosts)
|
344
|
+
list = []
|
345
|
+
|
346
|
+
hosts.each do |host|
|
347
|
+
begin
|
348
|
+
nv = NodeValidator.new(host, @connection_timeout)
|
349
|
+
|
350
|
+
# if node is already in cluster's node list,
|
351
|
+
# or already included in the list to be added, we should skip it
|
352
|
+
node = find_node_by_name(nv.name)
|
353
|
+
node ||= list.detect{|n| n.name == nv.name}
|
354
|
+
|
355
|
+
# make sure node is not already in the list to add
|
356
|
+
if node
|
357
|
+
# Duplicate node name found. This usually occurs when the server
|
358
|
+
# services list contains both internal and external IP addresses
|
359
|
+
# for the same node. Add new host to list of alias filters
|
360
|
+
# and do not add new node.
|
361
|
+
node.reference_count.update{|v| v + 1}
|
362
|
+
node.add_alias(host)
|
363
|
+
add_alias(host, node)
|
364
|
+
next
|
365
|
+
end
|
366
|
+
|
367
|
+
node = create_node(nv)
|
368
|
+
list << node
|
369
|
+
|
370
|
+
rescue => e
|
371
|
+
Aerospike.logger.warn("Add node #{node.to_s} failed: #{e}")
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
list
|
376
|
+
end
|
377
|
+
|
378
|
+
def create_node(nv)
|
379
|
+
Node.new(self, nv)
|
380
|
+
end
|
381
|
+
|
382
|
+
def find_nodes_to_remove(refresh_count)
|
383
|
+
node_list = nodes
|
384
|
+
|
385
|
+
remove_list = []
|
386
|
+
|
387
|
+
node_list.each do |node|
|
388
|
+
if !node.active?
|
389
|
+
# Inactive nodes must be removed.
|
390
|
+
remove_list << node
|
391
|
+
next
|
392
|
+
end
|
393
|
+
|
394
|
+
case node_list.length
|
395
|
+
when 1
|
396
|
+
# Single node clusters rely solely on node health.
|
397
|
+
remove_list << node if node.unhealthy?
|
398
|
+
|
399
|
+
when 2
|
400
|
+
# Two node clusters require at least one successful refresh before removing.
|
401
|
+
if refresh_count == 1 && node.reference_count.value == 0 && !node.responded.value
|
402
|
+
# Node is not referenced nor did it respond.
|
403
|
+
remove_list << node
|
404
|
+
end
|
405
|
+
|
406
|
+
else
|
407
|
+
# Multi-node clusters require two successful node refreshes before removing.
|
408
|
+
if refresh_count >= 2 && node.reference_count.value == 0
|
409
|
+
# Node is not referenced by other nodes.
|
410
|
+
# Check if node responded to info request.
|
411
|
+
if node.responded.value
|
412
|
+
# Node is alive, but not referenced by other nodes. Check if mapped.
|
413
|
+
if !find_node_in_partition_map(node)
|
414
|
+
# Node doesn't have any partitions mapped to it.
|
415
|
+
# There is not point in keeping it in the cluster.
|
416
|
+
remove_list << node
|
417
|
+
end
|
418
|
+
else
|
419
|
+
# Node not responding. Remove it.
|
420
|
+
remove_list << node
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
remove_list
|
427
|
+
end
|
428
|
+
|
429
|
+
def find_node_in_partition_map(filter)
|
430
|
+
partitions_list = partitions
|
431
|
+
|
432
|
+
partitions_list.each do |node_array|
|
433
|
+
max = node_array.length
|
434
|
+
|
435
|
+
for i in 0...max
|
436
|
+
node = node_array[i]
|
437
|
+
# Use reference equality for performance.
|
438
|
+
if node == filter
|
439
|
+
return true
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
false
|
444
|
+
end
|
445
|
+
|
446
|
+
def add_nodes(nodes_to_add)
|
447
|
+
# Add all nodes at once to avoid copying entire array multiple times.
|
448
|
+
nodes_to_add.each do |node|
|
449
|
+
add_aliases(node)
|
450
|
+
end
|
451
|
+
|
452
|
+
add_nodes_copy(nodes_to_add)
|
453
|
+
end
|
454
|
+
|
455
|
+
def add_aliases(node)
|
456
|
+
# Add node's aliases to global alias set.
|
457
|
+
# Aliases are only used in tend thread, so synchronization is not necessary.
|
458
|
+
node.get_aliases.each do |aliass|
|
459
|
+
@aliases[aliass] = node
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
def add_nodes_copy(nodes_to_add)
|
464
|
+
@mutex.synchronize do
|
465
|
+
@cluster_nodes.concat(nodes_to_add)
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
def remove_nodes(nodes_to_remove)
|
470
|
+
# There is no need to delete nodes from partition_write_map because the nodes
|
471
|
+
# have already been set to inactive. Further connection requests will result
|
472
|
+
# in an exception and a different node will be tried.
|
473
|
+
|
474
|
+
# Cleanup node resources.
|
475
|
+
nodes_to_remove.each do |node|
|
476
|
+
# Remove node's aliases from cluster alias set.
|
477
|
+
# Aliases are only used in tend thread, so synchronization is not necessary.
|
478
|
+
node.get_aliases.each do |aliass|
|
479
|
+
Aerospike.logger.debug("Removing alias #{aliass}")
|
480
|
+
remove_alias(aliass)
|
481
|
+
end
|
482
|
+
|
483
|
+
node.close
|
484
|
+
end
|
485
|
+
|
486
|
+
# Remove all nodes at once to avoid copying entire array multiple times.
|
487
|
+
remove_nodes_copy(nodes_to_remove)
|
488
|
+
end
|
489
|
+
|
490
|
+
def set_nodes(nodes)
|
491
|
+
@mutex.synchronize do
|
492
|
+
# Replace nodes with copy.
|
493
|
+
@cluster_nodes = nodes
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
def remove_nodes_copy(nodes_to_remove)
|
498
|
+
# Create temporary nodes array.
|
499
|
+
# Since nodes are only marked for deletion using node references in the nodes array,
|
500
|
+
# and the tend thread is the only thread modifying nodes, we are guaranteed that nodes
|
501
|
+
# in nodes_to_remove exist. Therefore, we know the final array size.
|
502
|
+
nodes_list = nodes
|
503
|
+
node_array = []
|
504
|
+
count = 0
|
505
|
+
|
506
|
+
# Add nodes that are not in remove list.
|
507
|
+
nodes_list.each do |node|
|
508
|
+
if node_exists(node, nodes_to_remove)
|
509
|
+
Aerospike.logger.info("Removed node `#{node}`")
|
510
|
+
else
|
511
|
+
node_array[count] = node
|
512
|
+
count += 1
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
# Do sanity check to make sure assumptions are correct.
|
517
|
+
if count < node_array.length
|
518
|
+
Aerospike.logger.warn("Node remove mismatch. Expected #{node_array.length}, Received #{count}")
|
519
|
+
|
520
|
+
# Resize array.
|
521
|
+
node_array = node_array.dup[0..count-1]
|
522
|
+
end
|
523
|
+
|
524
|
+
set_nodes(node_array)
|
525
|
+
end
|
526
|
+
|
527
|
+
def node_exists(search, node_list)
|
528
|
+
node_list.any? {|node| node == search }
|
529
|
+
end
|
530
|
+
|
531
|
+
def find_node_by_name(node_name)
|
532
|
+
nodes.detect{|node| node.name == node_name }
|
533
|
+
end
|
534
|
+
|
535
|
+
end
|
536
|
+
|
537
|
+
end
|