aerospike 2.19.0 → 2.21.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.
@@ -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