pq-wsm 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +86 -0
  3. data/Rakefile +38 -0
  4. data/lib/generators/pc_queues/install_generator.rb +9 -0
  5. data/lib/generators/pc_queues/migration_generator.rb +15 -0
  6. data/lib/generators/pc_queues/templates/README +15 -0
  7. data/lib/generators/pc_queues/templates/initializer.rb +6 -0
  8. data/lib/generators/pc_queues/templates/migration.rb +36 -0
  9. data/lib/pc_queues.rb +43 -0
  10. data/lib/pc_queues/acts_as_enqueable.rb +31 -0
  11. data/lib/pc_queues/acts_as_queue_owner.rb +30 -0
  12. data/lib/pc_queues/priority_queue_item.rb +13 -0
  13. data/lib/pc_queues/queue.rb +312 -0
  14. data/lib/pc_queues/queue_item.rb +58 -0
  15. data/lib/pc_queues/queue_rule.rb +32 -0
  16. data/lib/pc_queues/queue_rule_set.rb +40 -0
  17. data/lib/pc_queues/queue_rules/boolean_queue_rule.rb +34 -0
  18. data/lib/pc_queues/queue_rules/numeric_queue_rule.rb +42 -0
  19. data/lib/pc_queues/queue_rules/sample_queue_rule.rb +36 -0
  20. data/lib/pc_queues/queue_rules/string_match_queue_rule.rb +44 -0
  21. data/lib/pc_queues/railtie.rb +10 -0
  22. data/lib/pc_queues/version.rb +3 -0
  23. data/lib/tasks/pc_queues_tasks.rake +4 -0
  24. data/spec/boolean_queue_rule_spec.rb +34 -0
  25. data/spec/numeric_queue_rule_spec.rb +123 -0
  26. data/spec/queue_spec.rb +494 -0
  27. data/spec/sample_queue_rule_spec.rb +34 -0
  28. data/spec/spec_helper.rb +30 -0
  29. data/spec/string_match_rule_spec.rb +76 -0
  30. data/spec/support/active_record.rb +42 -0
  31. data/spec/support/application.rb +122 -0
  32. data/spec/support/queue_helpers.rb +16 -0
  33. data/spec/support/redis_helpers.rb +32 -0
  34. data/spec/support/time.rb +23 -0
  35. metadata +131 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3c57d60c589e660e163c69281336d7df78c743c4
4
+ data.tar.gz: 2db6479616a377cd2d0b5e38390aa35463ba4983
5
+ SHA512:
6
+ metadata.gz: 57a65396a0b836c2c4380c11276b0789af30807abd8523b83ae91b24690208eaeec17396ed6ed4a686893dbc82fe38686e1f3fbaaab4f5b421c368fd04500b38
7
+ data.tar.gz: 37770ac8c7cbee10c60d8fedc599d5f3c492f424614c439c941ee6936fb087eb3232a1ed1589d7b57082a77afab1384c37491c264fd7a1fe4e06c34a3199b27a
data/README.md ADDED
@@ -0,0 +1,86 @@
1
+ ## PcQueues
2
+
3
+ A Queue Management Gem with Redis backing.
4
+
5
+ Copyright (c) ProctorCam Inc. 2014 All rights reserved
6
+
7
+ ### Installation
8
+
9
+ Dependency: Rails, Database, PubNub
10
+
11
+ Add the following to your Gemfile
12
+
13
+ ```ruby
14
+ gem 'pc-queues', :git => 'git@gitlab.proctorcam.com:proctorcam-development/pc-queues', :branch => 'develop'
15
+ ```
16
+
17
+ Run the install and migration generators
18
+
19
+ ```bash
20
+ > rails g pc_queues:install
21
+ > rails g pc_queues:migration
22
+ > rake db:migrate
23
+ ```
24
+
25
+ Update the initializer in config/initializers/pc_queues.rb to initialize the PubNub instance used.
26
+
27
+ That's it - you're done setting up.
28
+
29
+ ### Getting Started
30
+
31
+ Once you've got the gem setup. You'll designate Models as Enqueable and as Queue owners. Classes that are Enqueueable
32
+ can be enqueued and dequeued from a Queue Classes that are Queue owners can have has_one and has_many relationships to
33
+ Queues.
34
+
35
+ Queues are setup for single table inheritance, so you can derive from Queue to handle Class specific behavior.
36
+
37
+ An ActiveRecordClass can be both a Queue owner and Enqueable.
38
+
39
+ #### Setting Up to be a Queue Owner
40
+
41
+ ```ruby
42
+ require 'pc_queues'
43
+
44
+ class MyQueue < PcQueues::Queue
45
+ end
46
+
47
+ class MyModel < ActiveRecord::Base
48
+ include ACTS_AS_QUEUE_OWNER
49
+
50
+ has_many_queues_as :my_queues
51
+ has_one_queue_as :fast_stuff, class_name: 'MyQueue'
52
+ end
53
+ ```
54
+
55
+ #### Setting Up to be a Queue Item
56
+
57
+ ```ruby
58
+ require 'pc_queues'
59
+
60
+ class MyModel < ActiveRecord::Base
61
+ include ACTS_AS_ENQUEABLE
62
+
63
+ end
64
+ ```
65
+
66
+ #### Queue Manipulation
67
+
68
+ ```ruby
69
+ model = MyModel.create attr1: value1
70
+
71
+ # has_one
72
+ model.create_fast_stuff attr1: value1
73
+
74
+ # has_many
75
+ model.my_queues.create attr1: value1
76
+
77
+ # Enqueue/Dequeue
78
+ model.fast_stuff.enqueue my_acts_as_enqueuable_object
79
+
80
+ my_acts_as_enqueuable_object = model.fast_stuff.dequeue
81
+
82
+
83
+ # Queue Length
84
+ length = model.fast_stuff.length
85
+
86
+ ```
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'PcQueues'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+
26
+ Bundler::GemHelper.install_tasks
27
+
28
+ require 'rake/testtask'
29
+
30
+ Rake::TestTask.new(:test) do |t|
31
+ t.libs << 'lib'
32
+ t.libs << 'test'
33
+ t.pattern = 'test/**/*_test.rb'
34
+ t.verbose = false
35
+ end
36
+
37
+
38
+ task :default => :test
@@ -0,0 +1,9 @@
1
+ class PcQueues::InstallGenerator < ::Rails::Generators::Base
2
+ source_root File.expand_path('../templates', __FILE__)
3
+ desc "Installs PcQueues."
4
+
5
+ def install
6
+ template "initializer.rb", "config/initializers/pc_queues.rb"
7
+ readme "README"
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ class PcQueues::MigrationGenerator < ::Rails::Generators::Base
4
+ include Rails::Generators::Migration
5
+ source_root File.expand_path('../templates', __FILE__)
6
+ desc "Installs PcQueues Gem migration file."
7
+
8
+ def install
9
+ migration_template 'migration.rb', 'db/migrate/create_pc_queues_tables.rb'
10
+ end
11
+
12
+ def self.next_migration_number(dirname)
13
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ ===============================================================================
2
+
3
+ There is a setup that will be needed before you use pc-queues.
4
+
5
+ 1 - You'll need to edit config/initializers/pc_queues.rb to setup the PubNub instance
6
+ to use when publishing Queue activity and managing presence info.
7
+
8
+ 2 - Run the migration generator and migrate
9
+
10
+ > rails g pc_queues:migration
11
+ > rake db:migrate
12
+
13
+ That's it, that's all. Enjoy!
14
+
15
+ ===============================================================================
@@ -0,0 +1,6 @@
1
+ require 'pc_queues'
2
+
3
+ PcQueues.config do |config|
4
+ # PcQueues uses pubnub to update interested clients when things change
5
+ # config.the_pubnub = Pubnub::the_pubnub
6
+ end
@@ -0,0 +1,36 @@
1
+ class CreatePcQueuesTables < ActiveRecord::Migration
2
+ def change
3
+ create_table :queues do |t|
4
+ t.string :name
5
+ t.string :type
6
+ t.boolean :is_in_cold_start, :default => false
7
+ t.integer :queue_owner_id
8
+ t.string :queue_owner_type
9
+ end
10
+
11
+ create_table :queue_items do |t|
12
+ t.integer :position
13
+ t.integer :enqueued_time
14
+ t.integer :queue_id
15
+ t.integer :enqueable_id
16
+ t.string :enqueable_type
17
+ t.string :type
18
+ end
19
+
20
+ create_table :queue_rule_sets do |t|
21
+ t.boolean :is_any, :default => false
22
+ t.integer :queue_id
23
+ end
24
+
25
+ create_table :queue_rules do |t|
26
+ t.string :type
27
+ t.string :name
28
+ t.integer :numeric_value
29
+ t.string :string_value
30
+ t.integer :queue_rule_set_id
31
+ t.boolean :bool_value
32
+ end
33
+ end
34
+ end
35
+
36
+
data/lib/pc_queues.rb ADDED
@@ -0,0 +1,43 @@
1
+ #
2
+ # This source file is part of project: PcQueues
3
+ #
4
+ # A Proctoring Workflow Platform
5
+ #
6
+ # Copyright (c) ProctorCam Inc. 2014 All rights reserved
7
+ #
8
+
9
+ module PcQueues
10
+ autoload :ActsAsQueueOwner, "pc_queues/acts_as_queue_owner"
11
+ autoload :ActsAsEnqueable, "pc_queues/acts_as_enqueable"
12
+ autoload :Queue, "pc_queues/queue"
13
+ autoload :QueueRule, "pc_queues/queue_rule"
14
+ autoload :PriorityQueueItem, "pc_queues/priority_queue_item"
15
+ autoload :QueueItem, "pc_queues/queue_item"
16
+ autoload :QueueRuleSet, "pc_queues/queue_rule_set"
17
+
18
+ module QueueRules
19
+ autoload :BooleanQueueRule, "pc_queues/queue_rules/boolean_queue_rule"
20
+ autoload :NumericQueueRule, "pc_queues/queue_rules/numeric_queue_rule"
21
+ autoload :SampleQueueRule, "pc_queues/queue_rules/sample_queue_rule"
22
+ autoload :StringMatchQueueRule, "pc_queues/queue_rules/string_match_queue_rule"
23
+ end
24
+
25
+ class << self
26
+ ##
27
+ # Configuration for PcQueues - see config/initializers/pc_queues.rb for details
28
+ #
29
+ def config
30
+ yield self
31
+ end
32
+
33
+ def the_pubnub=(pubnub)
34
+ @@pubnub = pubnub
35
+ end
36
+
37
+ def the_pubnub
38
+ @@pubnub
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,31 @@
1
+ #
2
+ # This source file is part of project: PcQueues
3
+ #
4
+ # A Proctoring Workflow Platform
5
+ #
6
+ # Copyright (c) ProctorCam Inc. 2014 All rights reserved
7
+ #
8
+ require 'active_support'
9
+
10
+ module PcQueues
11
+ module ActsAsEnqueable
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ acts_as_enqueuable
16
+ end
17
+
18
+ module ClassMethods
19
+ def acts_as_enqueuable
20
+ # instances are enqueued with a polymorphic PcQueues::QueueItem instance
21
+ has_many :enqueued_with, :class_name => 'PcQueues::QueueItem', :as => :enqueable
22
+
23
+ # queues will return the PcQueues::Queue instances that
24
+ # this object is in.
25
+ has_many :queues, :through => :enqueued_with
26
+
27
+ before_destroy { |record| record.queues.each { |q| q.remove(record) } }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ #
2
+ # This source file is part of project: PcQueues
3
+ #
4
+ # A Proctoring Workflow Platform
5
+ #
6
+ # Copyright (c) ProctorCam Inc. 2014 All rights reserved
7
+ #
8
+ require 'active_support'
9
+
10
+ module PcQueues
11
+ module ActsAsQueueOwner
12
+ extend ActiveSupport::Concern
13
+
14
+ module ClassMethods
15
+ def has_many_queues_as(symbol, options = {})
16
+ klass = options[:class_name] or symbol.to_s.camelize.singularize
17
+ # has_many symbol, options.merge(:as => :queue_owner, :dependent => :destroy, :conditions => {:type => klass})
18
+ has_many symbol, -> {where(type: klass)}, options.merge(:as => :queue_owner, :dependent => :destroy) #==at :condtions is deprecated in rails 4, so replaced it with this
19
+ end
20
+
21
+ def has_one_queue_as(symbol, options = {})
22
+ klass = options[:class_name] or symbol.to_s.camelize.singularize
23
+ has_one symbol, -> {where(type: klass)}, options.merge(:as => :queue_owner, :dependent => :destroy)
24
+ # has_one symbol, options.merge(:as => :queue_owner, :dependent => :destroy, :conditions => {:type => klass})
25
+ end
26
+
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,13 @@
1
+ #
2
+ # This source file is part of project: PcQueues
3
+ #
4
+ # A Proctoring Workflow Platform
5
+ #
6
+ # Copyright (c) ProctorCam Inc. 2014 All rights reserved
7
+ #
8
+ module PcQueues
9
+ # PriorityQueueItems are a flavor of QueueItems that
10
+ # have higher priority.
11
+ class PriorityQueueItem < QueueItem
12
+ end
13
+ end
@@ -0,0 +1,312 @@
1
+ #
2
+ # This source file is part of project: PcQueues
3
+ #
4
+ # A Proctoring Workflow Platform
5
+ #
6
+ # Copyright (c) ProctorCam Inc. 2014 All rights reserved
7
+ #
8
+ module PcQueues
9
+ # TODO: handle PubNub failures more gracefully
10
+
11
+ class Queue < ActiveRecord::Base
12
+ def self.accessible_attributes
13
+ [:name, :is_in_cold_start]
14
+ end
15
+ # attr_accessible :name, :is_in_cold_start
16
+
17
+ has_many :queue_items, dependent: :destroy, class_name: 'PcQueues::PriorityQueueItem'
18
+ has_many :priority_queue_items, dependent: :destroy, class_name: 'PcQueues::PriorityQueueItem'
19
+ has_many :queue_rule_sets, dependent: :destroy
20
+
21
+ belongs_to :queue_owner, :polymorphic => true
22
+
23
+ class_attribute :enqueue_callbacks, :dequeue_callbacks, :enqueable_type, :eager_load_relations
24
+
25
+ self.enqueue_callbacks = []
26
+ self.dequeue_callbacks = []
27
+ self.eager_load_relations = []
28
+
29
+ class << self
30
+
31
+ # An array of Queue Ids that have at least one client looking at them
32
+ def active_queue_ids
33
+ PubSub.proctors_for_class_instances(PcQueues::Queue).keys
34
+ end
35
+
36
+ # return Queue instances that have subscribers
37
+ def active_queues
38
+ Queue.find(active_queue_ids)
39
+ end
40
+
41
+ # Publish statistics for all "active" queues, where
42
+ # the length is non-zero and there are active subscribers.
43
+ #
44
+ # Returns the active queues
45
+ def publish_active_queues
46
+ aq = active_queues
47
+
48
+ aq.each do |queue|
49
+ PubSub.publish(
50
+ :channel => "pc-queue-#{queue.id}",
51
+ :message => {
52
+ :qualifier => "stats",
53
+ :data => queue.stats
54
+ }
55
+ ) { |data| }
56
+ end
57
+
58
+ aq
59
+ end
60
+
61
+ # Publish positions for all queue items in the queues provided
62
+ # if they have someone looking at them.
63
+ #
64
+ # The data published for each queue item is JSON-encoded:
65
+ # * position
66
+ # * estimated_time_left (in seconds)
67
+ #
68
+ def publish_positions(queues)
69
+ queues.each do |queue|
70
+ queue.publish_positions()
71
+ end
72
+ end
73
+
74
+ # Provide one or more callbacks to call before an item has been enqueued
75
+ #
76
+ # @param callbacks (symbol) the method name of the class that receives one
77
+ # parameters: the dequeued enqueable
78
+ def on_enqueue(*callbacks)
79
+ self.enqueue_callbacks = callbacks
80
+ end
81
+
82
+ # Provide one or more callbacks to call before an item has been dequeued
83
+ #
84
+ # @param callbacks (symbol) the method name of the class that receives one
85
+ # parameters: the dequeued enqueable
86
+ def on_dequeue(*callbacks)
87
+ self.dequeue_callbacks = callbacks
88
+ end
89
+
90
+ def enqueues(klass)
91
+ self.enqueable_type = klass
92
+ end
93
+
94
+ def eagerly_load(relations)
95
+ self.eager_load_relations = relations
96
+ end
97
+
98
+ end
99
+
100
+
101
+ # A list of all the elements in this queue
102
+ def enqueables(options = {})
103
+ @enqueables ||= queue_items.includes(:enqueable).where(options).map{ |queue_item| queue_item.enqueable }
104
+ end
105
+
106
+ def length()
107
+ # Due to Single Table Inheritance queue_items.count will count all types.
108
+ # This will include priority queue items as well.
109
+ queue_items.count
110
+ end
111
+
112
+
113
+ def priority_length()
114
+ priority_queue_items.count
115
+ end
116
+
117
+ # The current queue_item advancement rate
118
+ # Returns 0 if there is no item.
119
+ def rate()
120
+ first = queue_items.first
121
+ first and first.rate
122
+ end
123
+
124
+ # The current estimated wait time in seconds for items
125
+ # added to the queue. Returns 0 if there are no items.
126
+ def wait_time()
127
+ first = queue_items.first
128
+ first and first.wait_time
129
+ end
130
+
131
+ # Enqueue an Object onto the Queue
132
+ def enqueue(obj)
133
+ with_lock do
134
+ # queue_item = obj.enqueued_with.create enqueued_time: Time.now.to_i, position: (length + 1), :queue => self
135
+ queue_item = queue_items.create enqueued_time: Time.now.to_i, position: (length + 1), enqueable: obj
136
+
137
+ enqueue_queue_item queue_item
138
+ end
139
+
140
+ obj
141
+ end
142
+
143
+ # Enqueue an object onto the queue with high priority
144
+ # -> it will get dequeued before any enqueables that were
145
+ # added with `enqueue`
146
+ def priority_enqueue(obj)
147
+ with_lock do
148
+ queue_item = priority_queue_items.create enqueued_time: Time.now.to_i, position: (priority_length + 1), enqueable: obj
149
+
150
+ enqueue_queue_item queue_item
151
+ end
152
+
153
+ obj
154
+ end
155
+
156
+ # enqueue an object if it passes the rules for this queue
157
+ def enqueue_if_passes(enqueable, *args)
158
+ enqueue(enqueable) if rules_pass(enqueable, *args)
159
+ end
160
+
161
+ # Dequeue an Object from a Queue. The Object
162
+ # will no longer be in the Queue.
163
+ def dequeue()
164
+ obj = nil
165
+
166
+ while true do
167
+ begin
168
+ with_lock do
169
+ # Check priority_queue_items first, then queue_items
170
+ q_item = (priority_queue_items.first or queue_items.first)
171
+
172
+ return nil unless q_item
173
+
174
+ obj = q_item.enqueable
175
+
176
+ ActiveRecord::Associations::Preloader.new(obj, eager_load_relations).run
177
+
178
+ q_item.delete
179
+ end
180
+ rescue
181
+ # OK - Collided on a dequeued item, go try again.
182
+ next
183
+ end
184
+ # We've got an item, break
185
+ break
186
+ end
187
+
188
+ PubSub.publish(
189
+ :channel => "pc-queue-#{id}",
190
+ :message => [{
191
+ :qualifier => "dequeued",
192
+ :data => { :id => obj.id }
193
+ }, stats_message]
194
+ ) { |data| }
195
+
196
+ dequeue_callbacks.each { |callback| send callback, obj }
197
+
198
+ obj
199
+ end
200
+
201
+ # Remove a specific item from the Queue
202
+ def remove(obj)
203
+ begin
204
+ with_lock do
205
+ q_item = queue_items.where(enqueable_id: obj.id).first
206
+
207
+ return nil unless q_item
208
+
209
+ q_item.destroy
210
+ end
211
+ rescue
212
+ # Someone else beat us to the punch - just leave
213
+ return nil
214
+ end
215
+
216
+ PubSub.publish(
217
+ :channel => "pc-queue-#{id}",
218
+ :message => [{
219
+ :qualifier => "removed",
220
+ :data => { :id => obj.id }
221
+ }, stats_message ]
222
+ ) { |data| }
223
+
224
+ dequeue_callbacks.each { |callback| send callback, obj }
225
+
226
+ obj
227
+ end
228
+
229
+ # Determine if the rules for this queue pass
230
+ def rules_pass(enqueable, *args)
231
+ queue_rule_sets.includes(:queue_rules).all.each { |rule| return false unless rule.passes?(enqueable, *args) }
232
+ true
233
+ end
234
+
235
+ # Return the next object to be dequeued without dequeuing
236
+ def peek()
237
+ (item = queue_items.first)
238
+ item.cur_position = 1 unless item.nil?
239
+ item
240
+ end
241
+
242
+ # Current statistics for this queue
243
+ def stats
244
+ {
245
+ :length => length,
246
+ :rate => peek ? peek.rate : 0,
247
+ :longest_queue_wait => peek ? peek.wait_time : 0
248
+ }
249
+ end
250
+
251
+ # stats suitable for publishing
252
+ def stats_message
253
+ {
254
+ :qualifier => "stats",
255
+ :data => stats
256
+ }
257
+ end
258
+
259
+ def publish_stats
260
+ PcQueues::the_pubnub.publish(
261
+ :channel => "pc-queue-#{id}",
262
+ :message => stats_message
263
+ ) { |data| }
264
+ end
265
+
266
+ # Publish positions for this queue; see class method for more detail
267
+ def publish_positions
268
+ positionize()
269
+ queue_items.each do |item|
270
+ PubSub.publish(
271
+ :channel => "pc-queue-item-#{item.enqueable.id}",
272
+ :message => {
273
+ :qualifier => "stats",
274
+ :data => item.stats
275
+ }
276
+ ) { |data| }
277
+ end
278
+ end
279
+
280
+ protected
281
+
282
+ # DRY code for handling the common code between the regular and priority queues
283
+ def enqueue_queue_item(item)
284
+ # If validations fail, then the item is already in the queue.
285
+ return unless item.valid?
286
+
287
+ PubSub.publish(
288
+ :channel => "pc-queue-#{id}",
289
+ :message => [{
290
+ :qualifier => "enqueued",
291
+ :data => { :id => item.enqueable.id }
292
+ }, stats_message]
293
+ ) { |data| }
294
+
295
+ enqueue_callbacks.each { |callback| send callback, item.enqueable }
296
+ item.enqueable
297
+ end
298
+
299
+ # update the current position for Right Now of each queue item
300
+ def positionize
301
+ q_items = queue_items.select {|item| item.instance_of? ::PcQueues::QueueItem}
302
+ p_queue_items = queue_items.select {|item| item.instance_of? ::PcQueues::PriorityQueueItem}
303
+ (p_queue_items | q_items).each_with_index { |item, ndx| item.cur_position = ndx + 1 }
304
+ end
305
+
306
+ # Convert an array of objects to a hash by ids
307
+ def objects_to_id_hash(objects)
308
+ Hash[objects.collect {|o| [o.id, o]}]
309
+ end
310
+
311
+ end
312
+ end