aerospike 2.19.0 → 2.21.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,162 @@
1
+ # encoding: utf-8
2
+ # Copyright 2014-2020 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 'aerospike/command/admin_command'
18
+
19
+ module Aerospike
20
+
21
+ private
22
+
23
+ attr_reader :session_token, :session_expiration
24
+
25
+ class LoginCommand < AdminCommand #:nodoc:
26
+
27
+ def login(conn, policy)
28
+ hashed_pass = LoginCommand.hash_password(policy.password)
29
+ authenticate(conn, policy, hashed_pass)
30
+ end
31
+
32
+ def authenticate(conn, user, hashed_pass)
33
+ write_header(LOGIN, 2)
34
+ write_field_str(USER, policy.user)
35
+ write_field_bytes(CREDENTIAL, hashed_pass)
36
+
37
+ parse_tokens(conn)
38
+ end
39
+
40
+ def authenticate_new(conn, cluster)
41
+ policy = cluster.client_policy
42
+ case policy.auth_mode
43
+ when Aerospike::AuthMode::EXTERNAL
44
+ write_header(LOGIN, 3)
45
+ write_field_str(USER, policy.user)
46
+ write_field_bytes(CREDENTIAL, cluster.password)
47
+ write_field_str(CLEAR_PASSWORD, policy.password)
48
+ when Aerospike::AuthMode::INTERNAL
49
+ write_header(LOGIN, 2)
50
+ write_field_str(USER, policy.user)
51
+ write_field_bytes(CREDENTIAL, cluster.password)
52
+ when Aerospike::AuthMode::PKI
53
+ write_header(LOGIN, 0)
54
+ else
55
+ raise Exceptions::Aerospike.new(Aerospike::ResultCode::COMMAND_REJECTED, "Invalid client_policy#auth_mode.")
56
+ end
57
+
58
+ parse_tokens(conn)
59
+ cluster.session_token = @session_token
60
+ cluster.session_expiration = @session_expiration
61
+ end
62
+
63
+ def parse_tokens(conn)
64
+ begin
65
+ write_size
66
+ conn.write(@data_buffer, @data_offset)
67
+ conn.read(@data_buffer, HEADER_SIZE)
68
+
69
+ result = @data_buffer.read(RESULT_CODE)
70
+
71
+ if result != 0
72
+ return if result == Aerospike::ResultCode::SECURITY_NOT_ENABLED
73
+ raise Exceptions::Aerospike.new(result, "Authentication failed")
74
+ end
75
+
76
+ # read the rest of the buffer
77
+ size = @data_buffer.read_int64(0)
78
+ receive_size = (size & 0xFFFFFFFFFFFF) - HEADER_REMAINING
79
+ field_count = @data_buffer.read(11) & 0xFF
80
+
81
+ if receive_size <= 0 || receive_size > @data_buffer.size || field_count <= 0
82
+ raise Exceptions::Aerospike.new(result, "Node failed to retrieve session token")
83
+ end
84
+
85
+ if @data_buffer.size < receive_size
86
+ @data_buffer.resize(receive_size)
87
+ end
88
+
89
+ conn.read(@data_buffer, receive_size)
90
+
91
+ @data_offset = 0
92
+ for i in 0...field_count
93
+ mlen = @data_buffer.read_int32(@data_offset)
94
+ @data_offset += 4
95
+ id = @data_buffer.read(@data_offset)
96
+ @data_offset += 1
97
+ mlen -= 1
98
+
99
+ case id
100
+ when SESSION_TOKEN
101
+ # copy the contents of the buffer into a new byte slice
102
+ @session_token = @data_buffer.read(@data_offset, mlen)
103
+
104
+ when SESSION_TTL
105
+ # Subtract 60 seconds from TTL so client session expires before server session.
106
+ seconds = @data_buffer.read_int32(@data_offset) - 60
107
+
108
+ if seconds > 0
109
+ @session_expiration = Time.now + (seconds/86400)
110
+ else
111
+ Aerospike.logger.warn("Invalid session TTL: #{seconds}")
112
+ raise Exceptions::Aerospike.new(result, "Node failed to retrieve session token")
113
+ end
114
+ end
115
+
116
+ @data_offset += mlen
117
+ end
118
+
119
+ if !@session_token
120
+ raise Exceptions::Aerospike.new(result, "Node failed to retrieve session token")
121
+ end
122
+ ensure
123
+ Buffer.put(@data_buffer)
124
+ end
125
+ end
126
+
127
+ def authenticate_via_token(conn, cluster)
128
+ policy = cluster.client_policy
129
+ if policy.auth_mode != Aerospike::AuthMode::PKI
130
+ write_header(AUTHENTICATE, 2)
131
+ write_field_str(USER, policy.user)
132
+ else
133
+ write_header(AUTHENTICATE, 1)
134
+ end
135
+
136
+ write_field_bytes(SESSION_TOKEN, cluster.session_token) if cluster.session_token
137
+ write_size
138
+
139
+ conn.write(@data_buffer, @data_offset)
140
+ conn.read(@data_buffer, HEADER_SIZE)
141
+
142
+ result = @data_buffer.read(RESULT_CODE)
143
+ size = @data_buffer.read_int64(0)
144
+ receive_size = (size & 0xFFFFFFFFFFFF) - HEADER_REMAINING
145
+ conn.read(@data_buffer, receive_size)
146
+
147
+ if result != 0
148
+ return if result == Aerospike::ResultCode::SECURITY_NOT_ENABLED
149
+ raise Exceptions::Aerospike.new(result, "Authentication failed")
150
+ end
151
+
152
+ nil
153
+ end
154
+
155
+ SALT = '$2a$10$7EqJtq98hPqEX7fNZaFWoO'
156
+ def self.hash_password(password)
157
+ # Hashing the password with the cost of 10, with a static salt
158
+ return BCrypt::Engine.hash_secret(password, SALT, :cost => 10)
159
+ end
160
+ end
161
+ end
162
+
@@ -150,6 +150,23 @@ module Aerospike
150
150
  Aerospike::Key.new(namespace, set_name, user_key, digest)
151
151
  end
152
152
 
153
+ def skip_key(field_count)
154
+ # in Stream queries, there are no keys
155
+ return unless field_count > 0
156
+
157
+ i = 0
158
+ while i < field_count
159
+ read_bytes(4)
160
+
161
+ fieldlen = @data_buffer.read_int32(0)
162
+ read_bytes(fieldlen)
163
+
164
+ i = i.succ
165
+ end
166
+
167
+ nil
168
+ end
169
+
153
170
  # Parses the given byte buffer and populate the result object.
154
171
  # Returns the number of bytes that were parsed from the given buffer.
155
172
  def parse_record(key, op_count, generation, expiration)
@@ -21,9 +21,41 @@ module Aerospike
21
21
  module Connection # :nodoc:
22
22
  module Authenticate
23
23
  class << self
24
- def call(conn, user, password)
25
- command = AdminCommand.new
26
- command.authenticate(conn, user, password)
24
+ def call(conn, user, hashed_pass)
25
+ command = LoginCommand.new
26
+ command.authenticate(conn, user, hashed_pass)
27
+ true
28
+ rescue ::Aerospike::Exceptions::Aerospike
29
+ conn.close if conn
30
+ raise ::Aerospike::Exceptions::InvalidCredentials
31
+ end
32
+ end
33
+ end
34
+ module AuthenticateNew
35
+ class << self
36
+ INVALID_SESSION_ERR = [ResultCode::INVALID_CREDENTIAL,
37
+ ResultCode::EXPIRED_SESSION]
38
+
39
+ def call(conn, cluster)
40
+ command = LoginCommand.new
41
+ if !cluster.session_valid?
42
+ command.authenticate_new(conn, cluster)
43
+ else
44
+ begin
45
+ command.authenticate_via_token(conn, cluster)
46
+ rescue => ae
47
+ # always reset session info on errors to be on the safe side
48
+ cluster.reset_session_info
49
+ if ae.is_a?(Exceptions::Aerospike)
50
+ if INVALID_SESSION_ERR.include?(ae.result_code)
51
+ command.authenticate(conn, cluster)
52
+ return
53
+ end
54
+ end
55
+ raise ae
56
+ end
57
+ end
58
+
27
59
  true
28
60
  rescue ::Aerospike::Exceptions::Aerospike
29
61
  conn.close if conn
@@ -0,0 +1,36 @@
1
+ # encoding: utf-8
2
+ # Copyright 2014-2020 Aerospike, Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http:#www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ module Aerospike
17
+
18
+ module AuthMode
19
+
20
+ # INTERNAL uses internal authentication only when user/password defined. Hashed password is stored
21
+ # on the server. Do not send clear password. This is the default.
22
+ INTERNAL = 0
23
+
24
+ # EXTERNAL uses external authentication (like LDAP) when user/password defined. Specific external authentication is
25
+ # configured on server. If TLS is defined, sends clear password on node login via TLS.
26
+ # Will raise exception if TLS is not defined.
27
+ EXTERNAL = 1
28
+
29
+ # PKI allows authentication and authorization based on a certificate. No user name or
30
+ # password needs to be configured. Requires TLS and a client certificate.
31
+ # Requires server version 5.7.0+
32
+ PKI = 2
33
+
34
+ end # module
35
+
36
+ end # module
@@ -22,7 +22,7 @@ module Aerospike
22
22
  # Container object for client policy command.
23
23
  class ClientPolicy
24
24
 
25
- attr_accessor :user, :password
25
+ attr_accessor :user, :password, :auth_mode
26
26
  attr_accessor :timeout, :connection_queue_size, :fail_if_not_connected, :tend_interval
27
27
  attr_accessor :cluster_name
28
28
  attr_accessor :tls
@@ -44,6 +44,9 @@ module Aerospike
44
44
  # which the client checks for cluster state changes. Minimum interval is 10ms.
45
45
  self.tend_interval = opt[:tend_interval] || 1000 # 1 second
46
46
 
47
+ # Authentication mode
48
+ @auth_mode = opt[:auth_mode] || AuthMode::INTERNAL
49
+
47
50
  # user name
48
51
  @user = opt[:user]
49
52
 
@@ -0,0 +1,133 @@
1
+ # encoding: utf-8
2
+ # Copyright 2014-2022 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
+ module Aerospike
18
+
19
+ # Determines user access granularity.
20
+ class Privilege
21
+
22
+ # Role
23
+ attr_accessor :code
24
+
25
+ # Namespace determines namespace scope. Apply permission to this namespace only.
26
+ # If namespace is zero value, the privilege applies to all namespaces.
27
+ attr_accessor :namespace
28
+
29
+ # Set name scope. Apply permission to this set within namespace only.
30
+ # If set is zero value, the privilege applies to all sets within namespace.
31
+ attr_accessor :set_name
32
+
33
+ # Manage users and their roles.
34
+ USER_ADMIN = 'user-admin'
35
+
36
+ # Manage indicies, user-defined functions and server configuration.
37
+ SYS_ADMIN = 'sys-admin'
38
+
39
+ # Manage indicies and user defined functions.
40
+ DATA_ADMIN = 'data-admin'
41
+
42
+ # Manage user defined functions.
43
+ UDF_ADMIN = 'udf-admin'
44
+
45
+ # Manage indicies.
46
+ SINDEX_ADMIN = 'sindex-admin'
47
+
48
+ # Allow read, write and UDF transactions with the database.
49
+ READ_WRITE_UDF = "read-write-udf"
50
+
51
+ # Allow read and write transactions with the database.
52
+ READ_WRITE = 'read-write'
53
+
54
+ # Allow read transactions with the database.
55
+ READ = 'read'
56
+
57
+ # Write allows write transactions with the database.
58
+ WRITE = 'write'
59
+
60
+ # Truncate allow issuing truncate commands.
61
+ TRUNCATE = 'truncate'
62
+
63
+ def initialize(opt={})
64
+ @code = opt[:code]
65
+ @namespace = opt[:namespace]
66
+ @set_name = opt[:set_name]
67
+ end
68
+
69
+ def to_s
70
+ "code: #{@code}, namespace: #{@namespace}, set_name: #{@set_name}"
71
+ end
72
+
73
+ def to_code
74
+ case @code
75
+ when USER_ADMIN
76
+ 0
77
+ when SYS_ADMIN
78
+ 1
79
+ when DATA_ADMIN
80
+ 2
81
+ when UDF_ADMIN
82
+ 3
83
+ when SINDEX_ADMIN
84
+ 4
85
+ when READ
86
+ 10
87
+ when READ_WRITE
88
+ 11
89
+ when READ_WRITE_UDF
90
+ 12
91
+ when WRITE
92
+ 13
93
+ when TRUNCATE
94
+ 14
95
+ else
96
+ raise Exceptions::Aerospike.new(Aerospike::ResultCode::INVALID_PRIVILEGE, "Invalid role #{@code}")
97
+ end # case
98
+ end # def
99
+
100
+ def self.from(code)
101
+ case code
102
+ when 0
103
+ USER_ADMIN
104
+ when 1
105
+ SYS_ADMIN
106
+ when 2
107
+ DATA_ADMIN
108
+ when 3
109
+ UDF_ADMIN
110
+ when 4
111
+ SINDEX_ADMIN
112
+ when 10
113
+ READ
114
+ when 11
115
+ READ_WRITE
116
+ when 12
117
+ READ_WRITE_UDF
118
+ when 13
119
+ WRITE
120
+ when 14
121
+ TRUNCATE
122
+ else
123
+ raise Exceptions::Aerospike.new(Aerospike::ResultCode::INVALID_PRIVILEGE, "Invalid code #{code}")
124
+ end # case
125
+ end # def
126
+
127
+ def can_scope?
128
+ to_code >= 10
129
+ end
130
+
131
+ end # class
132
+
133
+ end
@@ -23,12 +23,13 @@ module Aerospike
23
23
 
24
24
  class QueryCommand < StreamCommand #:nodoc:
25
25
 
26
- def initialize(node, policy, statement, recordset)
26
+ def initialize(node, policy, statement, recordset, partitions)
27
27
  super(node)
28
28
 
29
29
  @policy = policy
30
30
  @statement = statement
31
31
  @recordset = recordset
32
+ @partitions = partitions
32
33
  end
33
34
 
34
35
  def write_buffer
@@ -81,7 +82,10 @@ module Aerospike
81
82
  @data_offset += binNameSize
82
83
  fieldCount+=1
83
84
  end
84
- else
85
+ else
86
+ @data_offset += @partitions.length * 2 + FIELD_HEADER_SIZE
87
+ fieldCount += 1
88
+
85
89
  if @policy.records_per_second > 0
86
90
  @data_offset += 4 + FIELD_HEADER_SIZE
87
91
  fieldCount += 1
@@ -89,8 +93,8 @@ module Aerospike
89
93
 
90
94
  # Calling query with no filters is more efficiently handled by a primary index scan.
91
95
  # Estimate scan options size.
92
- @data_offset += (2 + FIELD_HEADER_SIZE)
93
- fieldCount+=1
96
+ # @data_offset += (2 + FIELD_HEADER_SIZE)
97
+ # fieldCount+=1
94
98
  end
95
99
 
96
100
  @statement.set_task_id
@@ -177,18 +181,24 @@ module Aerospike
177
181
  end
178
182
  end
179
183
  else
184
+ write_field_header(@partitions.length * 2, Aerospike::FieldType::PID_ARRAY)
185
+ for pid in @partitions
186
+ @data_buffer.write_uint16_little_endian(pid, @data_offset)
187
+ @data_offset += 2
188
+ end
189
+
180
190
  if @policy.records_per_second > 0
181
191
  write_field_int(@policy.records_per_second, Aerospike::FieldType::RECORDS_PER_SECOND)
182
192
  end
183
193
 
184
194
  # Calling query with no filters is more efficiently handled by a primary index scan.
185
- write_field_header(2, Aerospike::FieldType::SCAN_OPTIONS)
186
- priority = @policy.priority.ord
187
- priority = priority << 4
188
- @data_buffer.write_byte(priority, @data_offset)
189
- @data_offset+=1
190
- @data_buffer.write_byte(100.ord, @data_offset)
191
- @data_offset+=1
195
+ # write_field_header(2, Aerospike::FieldType::SCAN_OPTIONS)
196
+ # priority = @policy.priority.ord
197
+ # priority = priority << 4
198
+ # @data_buffer.write_byte(priority, @data_offset)
199
+ # @data_offset+=1
200
+ # @data_buffer.write_byte(100.ord, @data_offset)
201
+ # @data_offset+=1
192
202
  end
193
203
 
194
204
  write_field_header(8, Aerospike::FieldType::TRAN_ID)
@@ -23,7 +23,7 @@ module Aerospike
23
23
 
24
24
  class ScanCommand < StreamCommand #:nodoc:
25
25
 
26
- def initialize(node, policy, namespace, set_name, bin_names, recordset)
26
+ def initialize(node, policy, namespace, set_name, bin_names, recordset, partitions)
27
27
  super(node)
28
28
 
29
29
  @policy = policy
@@ -31,10 +31,11 @@ module Aerospike
31
31
  @set_name = set_name
32
32
  @bin_names = bin_names
33
33
  @recordset = recordset
34
+ @partitions = partitions
34
35
  end
35
36
 
36
37
  def write_buffer
37
- set_scan(@policy, @namespace, @set_name, @bin_names)
38
+ set_scan(@policy, @namespace, @set_name, @bin_names, @partitions)
38
39
  end
39
40
 
40
41
  end # class
@@ -58,6 +58,9 @@ module Aerospike
58
58
  op_count = @data_buffer.read_int16(20)
59
59
  key = parse_key(field_count)
60
60
 
61
+ # If cmd is the end marker of the response, do not proceed further
62
+ return true if (info3 & INFO3_PARTITION_DONE) != 0
63
+
61
64
  if result_code == 0
62
65
  if @recordset.active?
63
66
  @recordset.records.enq(parse_record(key, op_count, generation, expiration))
@@ -182,7 +182,7 @@ module Aerospike
182
182
  # Privilege is invalid.
183
183
  INVALID_PRIVILEGE = 72
184
184
 
185
- # Specified IP whitelist is invalid.
185
+ # Specified IP allowlist is invalid.
186
186
  INVALID_WHITELIST = 73
187
187
 
188
188
  # User must be authentication before performing database operations.
@@ -191,7 +191,7 @@ module Aerospike
191
191
  # User does not posses the required role to perform the database operation.
192
192
  ROLE_VIOLATION = 81
193
193
 
194
- # Client IP address is not on the IP whitelist.
194
+ # Client IP address is not on the IP allowlist.
195
195
  NOT_WHITELISTED = 82
196
196
 
197
197
  # LDAP feature not enabled on server.
@@ -422,7 +422,7 @@ module Aerospike
422
422
  "Invalid privilege"
423
423
 
424
424
  when INVALID_WHITELIST
425
- "Specified IP whitelist is invalid"
425
+ "Specified IP allowlist is invalid"
426
426
 
427
427
  when NOT_AUTHENTICATED
428
428
  "Not authenticated"
@@ -431,7 +431,7 @@ module Aerospike
431
431
  "Role violation"
432
432
 
433
433
  when NOT_WHITELISTED
434
- "Client IP address is not on the IP whitelist"
434
+ "Client IP address is not on the IP allowlist"
435
435
 
436
436
  when LDAP_NOT_ENABLED
437
437
  "LDAP feature not enabled on server"
@@ -0,0 +1,55 @@
1
+ # encoding: utf-8
2
+ # Copyright 2014-2020 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
+ module Aerospike
18
+
19
+ # Role provides granular access to database entities for users.
20
+ class Role
21
+
22
+ # Role name
23
+ attr_accessor :name
24
+
25
+ # List of assigned privileges
26
+ attr_accessor :privileges
27
+
28
+ # List of allowable IP addresses
29
+ attr_accessor :allowlist
30
+
31
+ # Maximum reads per second limit for the role
32
+ attr_accessor :read_quota
33
+
34
+ # Maximum writes per second limit for the role
35
+ attr_accessor :write_quota
36
+
37
+ # The following aliases are for backward compatibility reasons
38
+ USER_ADMIN = Privilege::USER_ADMIN # :nodoc:
39
+ SYS_ADMIN = Privilege::SYS_ADMIN # :nodoc:
40
+ DATA_ADMIN = Privilege::DATA_ADMIN # :nodoc:
41
+ UDF_ADMIN = Privilege::UDF_ADMIN # :nodoc:
42
+ SINDEX_ADMIN = Privilege::SINDEX_ADMIN # :nodoc:
43
+ READ_WRITE_UDF = Privilege::READ_WRITE_UDF # :nodoc:
44
+ READ_WRITE = Privilege::READ_WRITE # :nodoc:
45
+ READ = Privilege::READ # :nodoc:
46
+ WRITE = Privilege::WRITE # :nodoc:
47
+ TRUNCATE = Privilege::TRUNCATE # :nodoc:
48
+
49
+ def to_s
50
+ "Role [name=#{@name}, privileges=#{@privileges}, allowlist=#{@allowlist}, readQuota=#{@read_quota}, writeQuota=#{@write_quota}]";
51
+ end
52
+
53
+ end # class
54
+
55
+ end # module
@@ -25,6 +25,31 @@ module Aerospike
25
25
  # List of assigned roles.
26
26
  attr_accessor :roles
27
27
 
28
+ # List of read statistics. List may be nil.
29
+ # Current statistics by offset are:
30
+ #
31
+ # 0: read quota in records per second
32
+ # 1: single record read transaction rate (TPS)
33
+ # 2: read scan/query record per second rate (RPS)
34
+ # 3: number of limitless read scans/queries
35
+ #
36
+ # Future server releases may add additional statistics.
37
+ attr_accessor :read_info
38
+
39
+ # List of write statistics. List may be nil.
40
+ # Current statistics by offset are:
41
+ #
42
+ # 0: write quota in records per second
43
+ # 1: single record write transaction rate (TPS)
44
+ # 2: write scan/query record per second rate (RPS)
45
+ # 3: number of limitless write scans/queries
46
+ #
47
+ # Future server releases may add additional statistics.
48
+ attr_accessor :write_info
49
+
50
+ # Number of currently open connections for the user
51
+ attr_accessor :conns_in_use
52
+
28
53
  end
29
54
 
30
55
  end