corosync 0.0.3 → 0.1.0.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.
data/lib/corosync/cpg.rb CHANGED
@@ -24,7 +24,7 @@ require 'corosync/cpg/member'
24
24
  # @example
25
25
  # require 'corosync/cpg'
26
26
  # cpg = Corosync::CPG.new('mygroup')
27
- # cpg.on_message do |message, member|
27
+ # cpg.on_message do |message, sender|
28
28
  # puts "Received #{message}"
29
29
  # end
30
30
  # puts "Member node IDs: #{cpg.members.map {|m| m.nodeid}.join(" ")}"
@@ -71,17 +71,12 @@ class Corosync::CPG
71
71
  # @return [void]
72
72
  def connect
73
73
  handle_ptr = FFI::MemoryPointer.new(Corosync.find_type(:cpg_handle_t))
74
- cs_error = Corosync.cpg_model_initialize(handle_ptr, Corosync::CPG_MODEL_V1, @model.pointer, nil);
75
- if cs_error != :ok then
76
- raise StandardError, "Received #{cs_error.to_s.upcase} attempting to connect to corosync"
77
- end
74
+ model_cast = Corosync::CpgModelDataT.new(@model.to_ptr)
75
+ Corosync.cs_send(:cpg_model_initialize, handle_ptr, Corosync::CPG_MODEL_V1, model_cast, nil)
78
76
  @handle = handle_ptr.read_uint64
79
77
 
80
78
  fd_ptr = FFI::MemoryPointer.new(:int)
81
- cs_error = Corosync.cpg_fd_get(@handle, fd_ptr)
82
- if cs_error != :ok then
83
- raise StandardError, "Received #{cs_error.to_s.upcase} attempting to get handle descriptor"
84
- end
79
+ Corosync.cs_send(:cpg_fd_get, @handle, fd_ptr)
85
80
  @fd = IO.new(fd_ptr.read_int)
86
81
  end
87
82
 
@@ -90,10 +85,7 @@ class Corosync::CPG
90
85
  def finalize
91
86
  return if @handle.nil?
92
87
 
93
- cs_error = Corosync.cpg_finalize(@handle)
94
- if cs_error != :ok then
95
- raise StandardError, "Received #{cs_error.to_s.upcase} attempting to perform finalize"
96
- end
88
+ Corosync.cs_send(:cpg_finalize, @handle)
97
89
 
98
90
  @group = nil
99
91
  @fd = nil
@@ -113,10 +105,7 @@ class Corosync::CPG
113
105
  cpg_name = Corosync::CpgName.new
114
106
  cpg_name[:value] = name
115
107
  cpg_name[:length] = name.size
116
- cs_error = Corosync.cpg_join(@handle, cpg_name)
117
- if cs_error != :ok then
118
- raise StandardError, "Received #{cs_error.to_s.upcase} attempting to join group"
119
- end
108
+ Corosync.cs_send(:cpg_join, @handle, cpg_name)
120
109
 
121
110
  @group = name
122
111
 
@@ -132,10 +121,7 @@ class Corosync::CPG
132
121
  cpg_name[:length] = @group.size
133
122
 
134
123
  # we can't join multiple groups, so I dont know why corosync requires you to specify the group name
135
- cs_error = Corosync.cpg_leave(@handle, cpg_name)
136
- if cs_error != :ok then
137
- raise StandardError, "Received #{cs_error.to_s.upcase} attempting to leave group"
138
- end
124
+ Corosync.cs_send(:cpg_leave, @handle, cpg_name)
139
125
 
140
126
  @group = nil
141
127
  end
@@ -151,11 +137,14 @@ class Corosync::CPG
151
137
  timeout = nil if timeout == -1
152
138
  select([@fd], [], [], timeout)
153
139
  end
154
- cs_error = Corosync.cpg_dispatch(@handle, Corosync::CS_DISPATCH_ONE_NONBLOCKING)
155
- return false if cs_error == :err_try_again
156
- if cs_error != :ok then
157
- raise StandardError, "Received #{cs_error.to_s.upcase} attempting perform dispatch"
140
+
141
+ begin
142
+ Corosync.cs_send!(:cpg_dispatch, @handle, Corosync::CS_DISPATCH_ONE_NONBLOCKING)
143
+ rescue Corosync::TryAgainError => e
144
+ raise e if e.depth > 1 # this exception is from a nested corosync function, not our cpg_dispatch we just called
145
+ return false
158
146
  end
147
+
159
148
  return true
160
149
  end
161
150
 
@@ -229,10 +218,7 @@ class Corosync::CPG
229
218
  # @return [Integer]
230
219
  def nodeid
231
220
  nodeid_p = FFI::MemoryPointer.new(:uint)
232
- cs_error = Corosync.cpg_local_get(@handle, nodeid_p)
233
- if cs_error != :ok then
234
- raise StandardError, "Received #{cs_error.to_s.upcase} attempting to get nodeid"
235
- end
221
+ Corosync.cs_send(:cpg_local_get, @handle, nodeid_p)
236
222
  nodeid_p.read_uint
237
223
  end
238
224
 
@@ -246,25 +232,21 @@ class Corosync::CPG
246
232
  cpg_name[:length] = @group.size
247
233
 
248
234
  iteration_handle_ptr = FFI::MemoryPointer.new(Corosync.find_type(:cpg_iteration_handle_t))
249
- cs_error = Corosync.cpg_iteration_initialize(@handle, Corosync::CPG_ITERATION_ONE_GROUP, cpg_name, iteration_handle_ptr)
250
- if cs_error != :ok then
251
- raise StandardError, "Received #{cs_error.to_s.upcase} attempting to initialize member iteration"
252
- end
235
+ Corosync.cs_send(:cpg_iteration_initialize, @handle, Corosync::CPG_ITERATION_ONE_GROUP, cpg_name, iteration_handle_ptr)
253
236
  iteration_handle = iteration_handle_ptr.read_uint64
254
237
 
255
238
  begin
256
239
  iteration_description = Corosync::CpgIterationDescriptionT.new
257
- while (cs_error = Corosync.cpg_iteration_next(iteration_handle, iteration_description.pointer)) == :ok do
258
- members << Corosync::CPG::Member.new(iteration_description)
259
- end
260
- if cs_error != :err_no_sections then
261
- raise StandardError, "Received #{cs_error.to_s.upcase} attempting to iterate group members"
240
+ begin
241
+ loop do
242
+ Corosync.cs_send(:cpg_iteration_next, iteration_handle, iteration_description)
243
+ members << Corosync::CPG::Member.new(iteration_description)
244
+ end
245
+ rescue Corosync::NoSectionsError
246
+ # signals end of iteration
262
247
  end
263
248
  ensure
264
- cs_error = Corosync.cpg_iteration_finalize(iteration_handle)
265
- if cs_error != :ok then
266
- raise StandardError, "Received #{cs_error.to_s.upcase} attempting to finalize member iteration"
267
- end
249
+ Corosync.cs_send(:cpg_iteration_finalize, iteration_handle)
268
250
  end
269
251
 
270
252
  members
@@ -292,9 +274,8 @@ class Corosync::CPG
292
274
  end
293
275
  iovec_len = messages.size
294
276
 
295
- cs_error = Corosync.cpg_mcast_joined(@handle, Corosync::CPG_TYPE_AGREED, iovec_list_p, iovec_len)
296
- if cs_error != :ok then
297
- raise StandardError, "Received #{cs_error.to_s.upcase} attempting to send a message"
298
- end
277
+ Corosync.cs_send(:cpg_mcast_joined, @handle, Corosync::CPG_TYPE_AGREED, iovec_list_p, iovec_len)
278
+
279
+ true
299
280
  end
300
281
  end
@@ -0,0 +1,34 @@
1
+ module Corosync
2
+
3
+ # Base class that all Corosync exceptions descend from.
4
+ # Corosync exception classes are programitcally generated from the `:cs_error_t` enum in `ffi/common.rb`. See that file for a comprehensive list of exception classes. The `:err_try_again` value will map to `Corosync::TryAgainError`. All exception classes follow the same pattern.
5
+ class Error < StandardError
6
+ Map = {}
7
+
8
+ # @!attribute [rw] depth
9
+ # The number of {Corosync.cs_send} methods the exception has passed through. This is important so that we don't rescue nested exceptions. For example, we call cs_send(:cpg_dispatch) which calls a callback which calls cs_send(:cpg_mcast_joined). If the cpg_mcast_joined were to raise an exception, and we had a rescue around the cpg_dispatch, we wouldn't know whether the exception came from cpg_mcast_joined or cpg_dispatch. However in this case the depth would be 2, and so we would know not to rescue it.
10
+ # @return [Fixnum] Number of cs_send methods the exception has passed through
11
+ def depth
12
+ @depth ||= 0
13
+ end
14
+ def depth=(value)
15
+ @depth = value
16
+ end
17
+ end
18
+
19
+ Corosync.enum_type(:cs_error_t).to_h.each do |name, value|
20
+ next if name == :ok
21
+
22
+ name_s = name.to_s.sub(/^err_/, '').capitalize.gsub(/_(.)/){|m| m[1].upcase} + 'Error'
23
+
24
+ c = Class.new(Error) do
25
+ const_set :VALUE, value
26
+ def value
27
+ self.class.const_get :VALUE
28
+ end
29
+ end
30
+ const_set name_s, c
31
+
32
+ Error::Map[name] = c
33
+ end
34
+ end
@@ -0,0 +1,143 @@
1
+ require File.expand_path('../../corosync.rb', __FILE__)
2
+ require File.expand_path('../../../ffi/quorum.rb', __FILE__)
3
+
4
+ # Quorum is used for tracking the health of the cluster.
5
+ # This simply reads the quorum state as defined by corosync. Whenever the node gains or loses quorum, a notification callback is called. You can also poll the quorum state instead of using a callback.
6
+ #
7
+ # ----
8
+ #
9
+ # @example
10
+ # require 'corosync/quorum'
11
+ # quorum = Corosync::Quorum.new
12
+ # quorum.on_notify do |quorate,member_list|
13
+ # puts "Cluster is#{quorate ? '' ' not'} quorate"
14
+ # puts " Members: #{member_list.join(' ')}"
15
+ # end
16
+ # quorum.connect
17
+ # loop do
18
+ # quorum.dispatch
19
+ # end
20
+
21
+ class Corosync::Quorum
22
+ # The IO object containing the file descriptor notifications come across.
23
+ # You can use this to check for activity, but do not read anything from it.
24
+ # @return [IO]
25
+ attr_reader :fd
26
+
27
+ # Creates a new Quorum instance
28
+ #
29
+ # @param connect [Boolean] Whether to join the cluster immediately. If not provided, you must call {#connect} and/or {#connect} later.
30
+ #
31
+ # @return [void]
32
+ def initialize(connect = false)
33
+ @handle = nil
34
+ @fd = nil
35
+
36
+ @callbacks = Corosync::QuorumCallbacksT.new
37
+ @callbacks[:quorum_notify_fn] = self.method(:callback_notify)
38
+
39
+ self.connect if connect
40
+ end
41
+
42
+ # Connect to the Quorum service
43
+ # @param start [Boolean] Whether to start listening for notifications (will not make initial call to callback).
44
+ # @return [void]
45
+ def connect(start = false)
46
+ handle_ptr = FFI::MemoryPointer.new(Corosync.find_type(:quorum_handle_t))
47
+ quorum_type_ptr = FFI::MemoryPointer.new(:uint32)
48
+
49
+ Corosync.cs_send(:quorum_initialize, handle_ptr, @callbacks, quorum_type_ptr)
50
+
51
+ @handle = handle_ptr.read_uint64
52
+
53
+ fd_ptr = FFI::MemoryPointer.new(:int)
54
+ Corosync.cs_send(:quorum_fd_get, @handle, fd_ptr)
55
+ @fd = IO.new(fd_ptr.read_int)
56
+
57
+ self.start if start
58
+ end
59
+
60
+ # Shuts down the connection to the Quorum service
61
+ # @return [void]
62
+ def finalize
63
+ return if @handle.nil?
64
+
65
+ Corosync.cs_send(:quorum_finalize, @handle)
66
+
67
+ @handle = nil
68
+ @fd = nil
69
+ end
70
+
71
+ # Start monitoring for changes to quorum status.
72
+ # This basically just enables triggering the callback. If not called you can still call {#quorate?} to get quorum state.
73
+ # @param initial_callback [Boolean] Whether to call the callback after start.
74
+ # @return [Boolean]
75
+ def start(initial_callback = false)
76
+ connect if @handle.nil?
77
+
78
+ Corosync.cs_send(:quorum_trackstart, @handle, Corosync::CS_TRACK_CHANGES)
79
+
80
+ if initial_callback and @callback_notify then
81
+ @callback_notify.call(quorate?, [])
82
+ end
83
+ end
84
+
85
+ # Stop monitoring for changes to quorum status.
86
+ # @return [void]
87
+ def stop
88
+ Corosync.cs_send(:quorum_trackstop, @handle)
89
+ end
90
+
91
+ # Checks for a single pending event and triggers the appropriate callback if found.
92
+ # @param timeout [Integer] How long to wait for an event.
93
+ # * +-1+: Indefinite. Wait forever
94
+ # * +0+: Non-blocking. If there isn't a pending event, return immediately
95
+ # * +>0+: Wait the specified number of seconds.
96
+ # @return [Boolean] Returns +True+ if an event was triggered. Otherwise +False+.
97
+ def dispatch(timeout = -1)
98
+ if !timeout != 0 then
99
+ timeout = nil if timeout == -1
100
+ select([@fd], [], [], timeout)
101
+ end
102
+
103
+ begin
104
+ Corosync.cs_send!(:quorum_dispatch, @handle, Corosync::CS_DISPATCH_ONE_NONBLOCKING)
105
+ rescue Corosync::TryAgainError => e
106
+ raise e if e.depth > 1 # this exception is from a nested corosync function, not our quorum_dispatch we just called
107
+ return false
108
+ end
109
+
110
+ return true
111
+ end
112
+
113
+ # Proc to call when quorum state changes.
114
+ # @param block [Proc] Proc to call when quorm state changes. Pass +Nil+ to disable the callback.
115
+ # @yieldparam quorate [Boolean] Whether cluster is quorate.
116
+ # @yieldparam members [Array<Fixnum>] Node ID of cluster members.
117
+ # @return [void]
118
+ def on_notify(&block)
119
+ @callback_notify = block
120
+ end
121
+ def callback_notify(handle, quorate, ring_id, view_list_entries, view_list_ptr)
122
+ return if !@callback_notify
123
+
124
+ view_list = view_list_ptr.read_array_of_type(FFI.find_type(:uint32), :read_uint32, view_list_entries)
125
+ #view_list = []
126
+ #view_list_entries.times do |i|
127
+ #view_list << (view_list_ptr + i * FFI.type_size(:uint32)).read_uint32
128
+ #end
129
+
130
+ @callback_notify.call(quorate > 0, view_list)
131
+ end
132
+ private :callback_notify
133
+
134
+ # Get node quorum status
135
+ # @return [Boolean] Whether node is quorate.
136
+ def getquorate
137
+ quorate_ptr = FFI::MemoryPointer.new(:int)
138
+ Corosync.cs_send(:quorum_getquorate, @handle, quorate_ptr)
139
+
140
+ quorate_ptr.read_int > 0
141
+ end
142
+ alias_method :quorate?, :getquorate
143
+ end
@@ -0,0 +1,218 @@
1
+ require File.expand_path('../../corosync.rb', __FILE__)
2
+ require File.expand_path('../../../ffi/votequorum.rb', __FILE__)
3
+
4
+ # Votequorum is used for tracking the health of the cluster.
5
+ # This monitors the quorum state as configured. Whenever the node gains or loses quorum, a notification callback is called. You can also poll the quorum state instead of using a callback.
6
+ #
7
+ # ----
8
+ #
9
+ # @example
10
+ # require 'corosync/votequorum'
11
+ # vq = Corosync::Votequorum.new
12
+ # vq.on_notify do |quorate,node_list|
13
+ # puts "Cluster is#{quorate ? '' ' not'} quorate"
14
+ # puts " Nodes:"
15
+ # node_list.each do |name,state|
16
+ # puts " #{name}=#{state}"
17
+ # end
18
+ # end
19
+ # vq.connect
20
+ # loop do
21
+ # vq.dispatch
22
+ # end
23
+ class Corosync::Votequorum
24
+ require 'ostruct'
25
+
26
+ # The IO object containing the file descriptor notifications come across.
27
+ # You can use this to check for activity, but do not read anything from it.
28
+ # @return [IO]
29
+ attr_reader :fd
30
+
31
+ # Creates a new Votequorum instance
32
+ #
33
+ # @param connect [Boolean] Whether to join the cluster immediately. If not provided, you must call {#connect} and/or {#connect} later.
34
+ #
35
+ # @return [void]
36
+ def initialize(connect = false)
37
+ @handle = nil
38
+ @fd = nil
39
+
40
+ @callbacks = Corosync::VotequorumCallbacksT.new
41
+ @callbacks[:votequorum_notify_fn] = self.method(:callback_notify)
42
+ @callbacks[:votequorum_expectedvotes_notify_fn] = self.method(:callback_expectedvotes_notify)
43
+
44
+ self.connect if connect
45
+ end
46
+
47
+ # Connect to the Votequorum service
48
+ # @param start [Boolean] Whether to start listening for notifications (will not make initial call to callback).
49
+ # @return [void]
50
+ def connect(start = false)
51
+ handle_ptr = FFI::MemoryPointer.new(Corosync.find_type(:votequorum_handle_t))
52
+
53
+ Corosync.cs_send(:votequorum_initialize, handle_ptr, @callbacks)
54
+
55
+ @handle = handle_ptr.read_uint64
56
+
57
+ fd_ptr = FFI::MemoryPointer.new(:int)
58
+ Corosync.cs_send(:votequorum_fd_get, @handle, fd_ptr)
59
+ @fd = IO.new(fd_ptr.read_int)
60
+
61
+ self.start if start
62
+ end
63
+
64
+ # Shuts down the connection to the Quorum service
65
+ # @return [void]
66
+ def finalize
67
+ return if @handle.nil?
68
+
69
+ Corosync.cs_send(:votequorum_finalize, @handle)
70
+
71
+ @handle = nil
72
+ @fd = nil
73
+ end
74
+
75
+ # Start monitoring for changes to quorum status/config.
76
+ # This basically just enables triggering the callback. If not called you can still call {#quorate?} to get quorum state.
77
+ # @param initial_callback [Boolean] Whether to call the callback after start.
78
+ # @return [Boolean]
79
+ def start(initial_callback = false)
80
+ connect if @handle.nil?
81
+
82
+ Corosync.cs_send(:votequorum_trackstart, @handle, 0, Corosync::CS_TRACK_CHANGES)
83
+
84
+ if initial_callback and @callback_notify then
85
+ @callback_notify.call(quorate?)
86
+ end
87
+ end
88
+
89
+ # Stop monitoring for changes.
90
+ # @return [void]
91
+ def stop
92
+ Corosync.cs_send(:votequorum_trackstop, @handle)
93
+ end
94
+
95
+ # Checks for a single pending event and triggers the appropriate callback if found.
96
+ # @param timeout [Integer] How long to wait for an event.
97
+ # * +-1+: Indefinite. Wait forever
98
+ # * +0+: Non-blocking. If there isn't a pending event, return immediately
99
+ # * +>0+: Wait the specified number of seconds.
100
+ # @return [Boolean] Returns +True+ if an event was triggered. Otherwise +False+.
101
+ def dispatch(timeout = -1)
102
+ if !timeout != 0 then
103
+ timeout = nil if timeout == -1
104
+ select([@fd], [], [], timeout)
105
+ end
106
+
107
+ begin
108
+ Corosync.cs_send(:votequorum_dispatch, @handle, Corosync::CS_DISPATCH_ONE_NONBLOCKING)
109
+ rescue Corosync::TryAgainError
110
+ return false
111
+ end
112
+
113
+ return true
114
+ end
115
+
116
+ # Proc to call when quorum state changes.
117
+ # @param block [Proc] Proc to call when quorm state changes. Pass +Nil+ to disable the callback.
118
+ # @yieldparam quorate [Boolean] Whether cluster is quorate.
119
+ # @yieldparam nodes [Hash] Hash of node IDs and their state.
120
+ # The state is one of :member, :dead, or :leaving
121
+ # @return [void]
122
+ def on_notify(&block)
123
+ @callback_notify = block
124
+ end
125
+ def callback_notify(handle, context, quorate, node_list_entries, node_list_ptr)
126
+ return if !@callback_notify
127
+
128
+ node_list = {}
129
+ node_list_entries.times do |i|
130
+ node = Corosync::VotequorumNodeT.new(node_list_ptr + i * Corosync::VotequorumNodeT.size)
131
+ node_list[node[:nodeid]] = {
132
+ Corosync::VOTEQUORUM_NODESTATE_MEMBER => :member,
133
+ Corosync::VOTEQUORUM_NODESTATE_DEAD => :dead,
134
+ Corosync::VOTEQUORUM_NODESTATE_LEAVING => :leaving,
135
+ }[node[:state]]
136
+ node_list[node[:nodeid]] = :UNKNOWN if node_list[node[:nodeid]].nil?
137
+ end
138
+
139
+ @callback_notify.call(quorate > 0, node_list)
140
+ end
141
+ private :callback_notify
142
+
143
+ # Proc to call when the number of expected votes changes.
144
+ # @param block [Proc] Proc to call when the expected votes changes. Pass +Nil+ to disable the callback.
145
+ # @yieldparam expected_votes [Integer] New number of expected votes.
146
+ # @return [void]
147
+ def on_expectedvotes_notify(&block)
148
+ @callback_expectedvotes_notify = block
149
+ end
150
+ def callback_expectedvotes_notify(handle, context, expected_votes)
151
+ return if !@callback_expectedvotes_notify
152
+
153
+ @callback_expectedvotes_notify.call(expected_votes)
154
+ end
155
+ private :callback_expectedvotes_notify
156
+
157
+ # Get the votequorum info about a node.
158
+ # The return openstruct will contain the following keys
159
+ # * node_id - Integer
160
+ # * node_state - Symbol: :member or :dead or :leaving
161
+ # * node_votes - Integer
162
+ # * node_expected_votes - Integer
163
+ # * highest_expected - Integer
164
+ # * total_votes - Integer
165
+ # * quorum - Integer
166
+ # * flags - Array<Symbol> where each symbol is one of: :twonode, :quorate, :wait_for_all, :last_man_standing, :auto_tie_breaker, :allow_downscale, :qdevice_registered, :qdevice_alive, :qdevice_cast_vote, or :qdevice_master_wins
167
+ # * qdevice_votes - Integer
168
+ # * qdevice_name - String
169
+ # @param node_id [Integer] The node id to look up. 0 for the current node.
170
+ # @return [OpenStruct]
171
+ def info(node_id = 0)
172
+ info = Corosync::VotequorumInfo.new
173
+
174
+ Corosync.cs_send(:votequorum_getinfo, @handle, node_id, info)
175
+
176
+ info = OpenStruct.new(Hash[info.members.zip(info.values)])
177
+
178
+ info.qdevice_name = info.qdevice_name.to_s
179
+
180
+ flags = info.flags
181
+ info.flags = []
182
+ [:twonode,:quorate,:wait_for_all,:last_man_standing,:auto_tie_breaker,:allow_downscale,:qdevice_registered,:qdevice_alive,:qdevice_cast_vote,:qdevice_master_wins].each do |flag_name|
183
+ flag_value = Corosync.const_get("VOTEQUORUM_INFO_#{flag_name.to_s.upcase}")
184
+ info.flags << flag_name if flags & flag_value >= 1
185
+ end
186
+
187
+ info
188
+ #Corosync::Votequorum::Info.new info
189
+ end
190
+
191
+ # Set the number of expected votes for this node
192
+ # @param count [Integer]
193
+ # @return [void]
194
+ def set_expected(count)
195
+ Corosync.cs_send(:votequorum_setexpected, @handle, count)
196
+ end
197
+ alias_method :expected=, :set_expected
198
+
199
+ # Set the number of votes contributed by the specified node.
200
+ # @param count [Integer]
201
+ # @param node_id [Integer] The node to modify
202
+ # @return [void]
203
+ def set_votes(count, node_id = 0)
204
+ Corosync.cs_send(:votequorum_setvotes, @handle, node_id, count)
205
+ end
206
+ # Set the number of votes contributed by this node.
207
+ # Shorthand for {#set_votes}(count)
208
+ def votes=(count)
209
+ set_votes(count)
210
+ end
211
+
212
+ # Get whether this node is quorate or not
213
+ # Shorthand for {#info}.flags.include?(:quorate)
214
+ # @return [Boolean]
215
+ def quorate?
216
+ self.info.flags.include?(:quorate)
217
+ end
218
+ end