vines-services 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/LICENSE +19 -0
  2. data/README +40 -0
  3. data/Rakefile +130 -0
  4. data/bin/vines-services +95 -0
  5. data/conf/config.rb +25 -0
  6. data/lib/vines/services/command/init.rb +209 -0
  7. data/lib/vines/services/command/restart.rb +14 -0
  8. data/lib/vines/services/command/start.rb +30 -0
  9. data/lib/vines/services/command/stop.rb +20 -0
  10. data/lib/vines/services/command/views.rb +26 -0
  11. data/lib/vines/services/component.rb +26 -0
  12. data/lib/vines/services/config.rb +105 -0
  13. data/lib/vines/services/connection.rb +120 -0
  14. data/lib/vines/services/controller/attributes_controller.rb +19 -0
  15. data/lib/vines/services/controller/base_controller.rb +99 -0
  16. data/lib/vines/services/controller/disco_info_controller.rb +61 -0
  17. data/lib/vines/services/controller/labels_controller.rb +17 -0
  18. data/lib/vines/services/controller/members_controller.rb +44 -0
  19. data/lib/vines/services/controller/messages_controller.rb +66 -0
  20. data/lib/vines/services/controller/probes_controller.rb +45 -0
  21. data/lib/vines/services/controller/services_controller.rb +81 -0
  22. data/lib/vines/services/controller/subscriptions_controller.rb +39 -0
  23. data/lib/vines/services/controller/systems_controller.rb +45 -0
  24. data/lib/vines/services/controller/transfers_controller.rb +58 -0
  25. data/lib/vines/services/controller/uploads_controller.rb +62 -0
  26. data/lib/vines/services/controller/users_controller.rb +127 -0
  27. data/lib/vines/services/core_ext/blather.rb +46 -0
  28. data/lib/vines/services/core_ext/couchrest.rb +33 -0
  29. data/lib/vines/services/indexer.rb +195 -0
  30. data/lib/vines/services/priority_queue.rb +94 -0
  31. data/lib/vines/services/roster.rb +70 -0
  32. data/lib/vines/services/storage/couchdb/fragment.rb +23 -0
  33. data/lib/vines/services/storage/couchdb/service.rb +170 -0
  34. data/lib/vines/services/storage/couchdb/system.rb +141 -0
  35. data/lib/vines/services/storage/couchdb/upload.rb +66 -0
  36. data/lib/vines/services/storage/couchdb/user.rb +137 -0
  37. data/lib/vines/services/storage/couchdb/vcard.rb +13 -0
  38. data/lib/vines/services/storage/couchdb.rb +157 -0
  39. data/lib/vines/services/storage.rb +33 -0
  40. data/lib/vines/services/throttle.rb +26 -0
  41. data/lib/vines/services/version.rb +7 -0
  42. data/lib/vines/services/vql/compiler.rb +94 -0
  43. data/lib/vines/services/vql/vql.citrus +115 -0
  44. data/lib/vines/services/vql/vql.rb +186 -0
  45. data/lib/vines/services.rb +71 -0
  46. data/test/config_test.rb +242 -0
  47. data/test/priority_queue_test.rb +23 -0
  48. data/test/storage/couchdb_test.rb +30 -0
  49. data/test/vql/compiler_test.rb +96 -0
  50. data/test/vql/vql_test.rb +233 -0
  51. data/web/coffeescripts/api.coffee +51 -0
  52. data/web/coffeescripts/commands.coffee +18 -0
  53. data/web/coffeescripts/files.coffee +315 -0
  54. data/web/coffeescripts/init.coffee +21 -0
  55. data/web/coffeescripts/services.coffee +356 -0
  56. data/web/coffeescripts/setup.coffee +503 -0
  57. data/web/coffeescripts/systems.coffee +371 -0
  58. data/web/images/default-service.png +0 -0
  59. data/web/images/linux.png +0 -0
  60. data/web/images/mac.png +0 -0
  61. data/web/images/run.png +0 -0
  62. data/web/images/windows.png +0 -0
  63. data/web/index.html +17 -0
  64. data/web/stylesheets/common.css +52 -0
  65. data/web/stylesheets/files.css +218 -0
  66. data/web/stylesheets/services.css +181 -0
  67. data/web/stylesheets/setup.css +117 -0
  68. data/web/stylesheets/systems.css +142 -0
  69. metadata +230 -0
@@ -0,0 +1,39 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module Controller
6
+ # Presence subscription requests are approved if the user has privilege to
7
+ # access the requested service JID. All subscriptions to the component
8
+ # itself are approved.
9
+ class SubscriptionsController < BaseController
10
+ register :subscription, :request?
11
+
12
+ def process
13
+ if approved?
14
+ from, to = node.from.stripped, node.to.stripped
15
+ stream.write(node.approve!)
16
+ stream.write(available(to, from))
17
+ else
18
+ stream.write(node.refuse!)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def approved?
25
+ return true if to_component?
26
+ found = Service.find_by_jid(node.to)
27
+ found && found.user?(node.from.stripped)
28
+ end
29
+
30
+ def available(from, to)
31
+ Blather::Stanza::Presence::Status.new.tap do |stanza|
32
+ stanza.from = from
33
+ stanza.to = to
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,45 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module Controller
6
+ class SystemsController < BaseController
7
+ register :iq, "/iq[@type='get' or @type='set']/ns:query",
8
+ 'ns' => 'http://getvines.com/protocol/systems'
9
+
10
+ private
11
+
12
+ def get
13
+ if name = node.elements.first['name']
14
+ forbidden! unless current_user.manage_systems? || from_system?(name)
15
+ send_doc(System.find_by_name(name))
16
+ else
17
+ forbidden! unless current_user.manage_systems?
18
+ send_result(rows: System.find_all)
19
+ end
20
+ end
21
+
22
+ # Agents are allowed to save system data for their machine only. No
23
+ # other user may change a system's description.
24
+ def save
25
+ obj = JSON.parse(node.elements.first.content)
26
+ fqdn = obj['fqdn'].downcase
27
+ forbidden! unless from_system?(fqdn)
28
+
29
+ id = "system:#{fqdn}"
30
+ system = System.find(id) || System.new(id: id)
31
+ system.ohai = obj
32
+ system.save
33
+ storage.index(system)
34
+ System.notify_members(stream, node.from, [{'name' => fqdn}])
35
+ send_result
36
+ end
37
+
38
+ # Return true if a System user is requesting access to its own data.
39
+ def from_system?(fqdn)
40
+ current_user.system? && node.from.node.downcase == fqdn.downcase
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,58 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module Controller
6
+ class TransfersController < BaseController
7
+ register :file_transfer
8
+
9
+ def process
10
+ forbidden! unless current_user.manage_files?
11
+ return unless node.to == stream.jid
12
+ name, size = node.si.file['name'], node.si.file['size'].to_i
13
+ log.debug("Receiving file: #{name}")
14
+
15
+ path = File.expand_path(name, uploads)
16
+ return unless path.start_with?(uploads)
17
+
18
+ log.debug("Saving file to #{path}")
19
+ File.delete(path) if File.exist?(path)
20
+
21
+ save_file_doc(name, size)
22
+
23
+ transfer = Blather::FileTransfer.new(stream, node)
24
+ transfer.allow_s5b = false
25
+ transfer.accept(FileUploader, path, size, storage)
26
+ end
27
+
28
+ private
29
+
30
+ def save_file_doc(name, size)
31
+ file = Upload.find_by_name(name) || Upload.new
32
+ file.name = name
33
+ file.size = size
34
+ file.save
35
+ end
36
+
37
+ module FileUploader
38
+ include Blather::FileTransfer::SimpleFileReceiver
39
+
40
+ def initialize(path, size, storage)
41
+ super(path, size)
42
+ @storage = storage
43
+ end
44
+
45
+ def unbind
46
+ super
47
+ return unless File.exist?(@path)
48
+ Fiber.new do
49
+ @storage.store_file(@path) do
50
+ File.delete(@path) rescue nil
51
+ end
52
+ end.resume
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,62 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module Controller
6
+ class UploadsController < BaseController
7
+ register :iq, "/iq[@type='get' or @type='set']/ns:query",
8
+ 'ns' => 'http://getvines.com/protocol/files'
9
+
10
+ private
11
+
12
+ def get
13
+ forbidden! unless current_user.manage_files?
14
+ id, name, label = %w[id name label].map {|a| node.elements.first[a] }
15
+
16
+ if id
17
+ send_doc(Upload.find(id))
18
+ elsif name
19
+ send_doc(Upload.find_by_name(name))
20
+ elsif label
21
+ send_result(rows: Upload.find_by_label(label))
22
+ else
23
+ send_result(rows: Upload.find_all)
24
+ end
25
+ end
26
+
27
+ # Uploaded files may have their names and labels updated, but not their
28
+ # size. New files are created by uploading them to the UploadsController,
29
+ # not by saving them here.
30
+ def save
31
+ forbidden! unless current_user.manage_files?
32
+ obj = JSON.parse(node.elements.first.content)
33
+ file = Upload.find(obj['id'])
34
+
35
+ unless file
36
+ send_error('item-not-found')
37
+ return
38
+ end
39
+
40
+ file.name = obj['name']
41
+ file.labels = obj['labels']
42
+ if file.valid?
43
+ file.save
44
+ send_doc(file)
45
+ else
46
+ send_error('not-acceptable')
47
+ end
48
+ end
49
+
50
+ def delete
51
+ forbidden! unless current_user.manage_files?
52
+ if file = Upload.find(node.elements.first['id'])
53
+ file.destroy
54
+ send_result
55
+ else
56
+ send_error('item-not-found')
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,127 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module Controller
6
+ class UsersController < BaseController
7
+ register :iq, "/iq[@type='get' or @type='set']/ns:query",
8
+ 'ns' => 'http://getvines.com/protocol/users'
9
+
10
+ private
11
+
12
+ # Returns user information to the client without the password. Passwords,
13
+ # despite being securely hashed, must never be sent to client requests.
14
+ def get
15
+ if jid = node.elements.first['jid']
16
+ user = User.find_by_jid(jid)
17
+ forbidden! unless authorized?(user)
18
+ send_doc(user)
19
+ else
20
+ users = User.find_all.select {|user| authorized?(user) }
21
+ send_result(rows: users)
22
+ end
23
+ end
24
+
25
+ # Save the user to the database. The password needs to be bcrypted
26
+ # before it is stored. Empty passwords will not be saved.
27
+ def save
28
+ obj = JSON.parse(node.elements.first.content)
29
+ forbidden! unless authorized?(obj)
30
+
31
+ system = (obj['system'] == true)
32
+ jid, name, username, password1, password2 =
33
+ %w[jid name username password1 password2].map {|a| (obj[a] || '').strip }
34
+
35
+ raise 'jid required' if jid.empty? && username.empty?
36
+
37
+ user = if jid.empty? # new user
38
+ id = "user:%s" % Vines::JID.new(username, node.from.domain)
39
+ raise 'user already exists' if User.find(id)
40
+ User.new(id: id).tap do |u|
41
+ u.system = system
42
+ u.password = password1
43
+ end
44
+ else # existing user
45
+ User.get!("user:%s" % Vines::JID.new(jid)).tap do |u|
46
+ raise "record found, but is a #{u.system ? 'system' : 'user'}" unless u.system == system
47
+ if system
48
+ u.password = password1 unless password1.empty?
49
+ else # humans
50
+ u.change_password(password1, password2) unless password1.empty? || password2.empty?
51
+ end
52
+ end
53
+ end
54
+
55
+ unless system
56
+ user.name = name
57
+ if user.jid == current_user.jid && !current_user.manage_services
58
+ if obj['permissions']['systems'] == true
59
+ user.valid = false
60
+ send_error('not-acceptable')
61
+ end
62
+ end
63
+ user.permissions = obj['permissions']
64
+ end
65
+
66
+ if user.valid?
67
+ user.save
68
+ save_services(user, obj['services'] || [])
69
+ send_doc(user)
70
+ else
71
+ send_error('not-acceptable')
72
+ end
73
+ end
74
+
75
+ # Return true if the user is allowed to view and save the system or
76
+ # user object. Users with no permissions still have the right to update
77
+ # their own user account.
78
+ def authorized?(user)
79
+ return false unless user
80
+ user = user.to_result if user.respond_to?(:to_result)
81
+ system = user['system'] || user[:system]
82
+ jid = user['jid'] || user[:jid]
83
+ return current_user.manage_systems? if system
84
+ current_user.manage_users? || current_user.jid == jid
85
+ end
86
+
87
+ def save_services(user, services)
88
+ return if user.system?
89
+ return unless user.permissions['manage_services']
90
+ current = user.services.map {|s| s.id }
91
+ members = []
92
+
93
+ # delete missing services
94
+ user.services.each do |service|
95
+ if !services.include?(service.id)
96
+ members << service.members
97
+ service.remove_user(user.jid)
98
+ service.save
99
+ end
100
+ end
101
+
102
+ # add new services
103
+ add = services.select {|id| !current.include?(id) }
104
+ Service.all.keys(add).each do |service|
105
+ members << service.members
106
+ service.add_user(user.jid)
107
+ service.save
108
+ end
109
+
110
+ System.notify_members(stream, node.from, members.flatten.uniq)
111
+ end
112
+
113
+ # Delete the user as defined by the id of the query stanza. Users may
114
+ # not delete themselves.
115
+ def delete
116
+ jid = Vines::JID.new(node.elements.first['id']).bare
117
+ raise 'jid required' if jid.empty?
118
+ user = User.find("user:#{jid}")
119
+ forbidden! unless authorized?(user) &&
120
+ node.from.stripped.to_s != jid.to_s
121
+ user.destroy
122
+ send_result
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,46 @@
1
+ # encoding: UTF-8
2
+
3
+ module Blather
4
+ class FileTransfer
5
+ class Ibb
6
+ # Override the accept method to write directly to the file system rather
7
+ # than using EM#attach. This is a workaround for this bug:
8
+ # https://github.com/eventmachine/eventmachine/issues/200
9
+ def accept(handler, *params)
10
+ klass = Class.new.send(:include, handler)
11
+ handler = klass.new(*params)
12
+ handler.post_init
13
+
14
+ @stream.register_handler :ibb_data, :from => @iq.from, :sid => @iq.sid do |iq|
15
+ if iq.data['seq'] == @seq.to_s
16
+ begin
17
+ handler.receive_data(Base64.decode64(iq.data.content))
18
+ @stream.write iq.reply
19
+ @seq += 1
20
+ @seq = 0 if @seq > 65535
21
+ rescue Exception => e
22
+ handler.unbind
23
+ @stream.write StanzaError.new(iq, 'not-acceptable', :cancel).to_node
24
+ end
25
+ else
26
+ handler.unbind
27
+ @stream.write StanzaError.new(iq, 'unexpected-request', :wait).to_node
28
+ end
29
+ true
30
+ end
31
+
32
+ @stream.register_handler :ibb_close, :from => @iq.from, :sid => @iq.sid do |iq|
33
+ @stream.write iq.reply
34
+ @stream.clear_handlers :ibb_data, :from => @iq.from, :sid => @iq.sid
35
+ @stream.clear_handlers :ibb_close, :from => @iq.from, :sid => @iq.sid
36
+ handler.unbind
37
+ true
38
+ end
39
+
40
+ @stream.clear_handlers :ibb_open, :from => @iq.from
41
+ @stream.clear_handlers :ibb_open, :from => @iq.from, :sid => @iq.sid
42
+ @stream.write @iq.reply
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: UTF-8
2
+
3
+ module CouchRest
4
+ module RestAPI
5
+
6
+ # Wrap a blocking IO method in a new method that pushes the original method
7
+ # onto EventMachine's thread pool using EM#defer. The calling Fiber is paused
8
+ # while the thread pool does its work, then resumed when the thread finishes.
9
+ def self.defer(method)
10
+ old = "_deferred_#{method}"
11
+ alias_method old, method
12
+ define_method method do |*args|
13
+ fiber = Fiber.current
14
+ op = proc do
15
+ begin
16
+ method(old).call(*args)
17
+ rescue
18
+ nil
19
+ end
20
+ end
21
+ cb = proc {|result| fiber.resume(result) }
22
+ EM.defer(op, cb)
23
+ Fiber.yield
24
+ end
25
+ end
26
+
27
+ # All CouchRest::RestAPI methods ultimately call execute to connect to
28
+ # CouchDB, using blocking IO. Push those blocking calls onto the thread
29
+ # pool so we don't block the reactor thread, but still get to use all
30
+ # of the goodness of CouchRest::Model.
31
+ defer :execute
32
+ end
33
+ end
@@ -0,0 +1,195 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ # Save system ohai documents in a sqlite database for fast searching. This
6
+ # index powers the live search feature of the service builder user interface.
7
+ # The indexing happens on a separate thread for two reasons:
8
+ # - writes to sqlite block the EventMachine thread
9
+ # - writes to sqlite lock the database file, so we need one writer process
10
+ class Indexer
11
+ include Vines::Log
12
+
13
+ @@indexers = {}
14
+
15
+ # Return the Indexer instance managing this file, creating a new Indexer
16
+ # instance if needed.
17
+ def self.[](file)
18
+ file = File.expand_path(file)
19
+ @@indexers[file] ||= self.new(file)
20
+ end
21
+
22
+ # There must be only one Indexer managing a sqlite file at one time.
23
+ # Use Indexer[file] to create or retrieve the Indexer for a given file
24
+ # rather than calling the constructor directly.
25
+ def initialize(file)
26
+ @db = database(file)
27
+ @tasks = PriorityQueue.new do |a, b|
28
+ a[:priority] <=> b[:priority]
29
+ end
30
+ process_tasks
31
+ end
32
+
33
+ # Queue the document for indexing at some point in the future. Because
34
+ # adding documents to the index is less time-sensitive than querying the
35
+ # index, these tasks may be delayed by query tasks.
36
+ def <<(doc)
37
+ @tasks.push({
38
+ priority: -Time.now.to_f,
39
+ type: :index,
40
+ doc: doc
41
+ })
42
+ end
43
+
44
+ # Run the SQL query with optional replacement parameters (e.g. ?) and yield
45
+ # the results array to the callback block. Queries are prioritized ahead of
46
+ # document indexing tasks so they will return quickly even when many documents
47
+ # are waiting to be indexed.
48
+ def find(query, *args, &callback)
49
+ @tasks.push({
50
+ priority: 0,
51
+ type: :query,
52
+ query: query,
53
+ args: args.flatten,
54
+ callback: callback
55
+ })
56
+ end
57
+
58
+ private
59
+
60
+ # Run the index processing loop, indexing one document at a time so that
61
+ # writes to sqlite are single threaded. Each task is performed in the
62
+ # EM thread pool so the reactor thread isn't blocked.
63
+ def process_tasks
64
+ @tasks.pop do |task|
65
+ callback = task[:callback]
66
+ op = proc do
67
+ case task[:type]
68
+ when :index then index(task[:doc])
69
+ when :query then query(task[:query], task[:args])
70
+ end
71
+ end
72
+ cb = proc do |results|
73
+ callback.call(results) if callback rescue nil
74
+ process_tasks
75
+ end
76
+ EM.defer(op, cb)
77
+ end
78
+ end
79
+
80
+ def query(sql, args)
81
+ @db.prepare(sql) do |stmt|
82
+ stmt.execute!(*args)
83
+ end
84
+ rescue Exception => e
85
+ log.error("Error searching index: #{e.message}")
86
+ []
87
+ end
88
+
89
+ def index(doc)
90
+ @db.transaction do
91
+ flat = flatten(doc)
92
+ system_id = find_or_create_system(doc)
93
+ attrs = find_attributes(system_id)
94
+ delete_attributes(flat, attrs, system_id)
95
+ update_attributes(flat, attrs, system_id)
96
+ insert_attributes(flat, attrs, system_id)
97
+ end
98
+ rescue Exception => e
99
+ log.error("Error indexing document: #{e.message}")
100
+ end
101
+
102
+ def find_or_create_system(doc)
103
+ @db.prepare("select id from systems where name=?") do |stmt|
104
+ row = stmt.execute!(name(doc)).first
105
+ row ? row[0] : insert_system(doc)
106
+ end
107
+ end
108
+
109
+ def insert_system(doc)
110
+ os = doc['kernel']['os'].downcase.sub('gnu/', '')
111
+ @db.prepare("insert into systems (name, os) values (?, ?)") do |stmt|
112
+ stmt.execute!(name(doc), os)
113
+ @db.last_insert_row_id
114
+ end
115
+ end
116
+
117
+ def find_attributes(system_id)
118
+ sql = "select key, value from attributes where system_id=?"
119
+ @db.prepare(sql) do |stmt|
120
+ {}.tap do |attrs|
121
+ stmt.execute!(system_id) do |row|
122
+ attrs[row[0]] = row[1]
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ def delete_attributes(flat, attrs, system_id)
129
+ deletes = attrs.keys.select {|k| !flat.key?(k) }
130
+ return if deletes.empty?
131
+ deletes.each_slice(999) do |slice|
132
+ params = slice.map{'?'}.join(',')
133
+ sql = "delete from attributes where system_id=? and key in (%s)" % params
134
+ @db.prepare(sql) do |stmt|
135
+ stmt.execute!(system_id, *slice)
136
+ end
137
+ end
138
+ end
139
+
140
+ def update_attributes(flat, attrs, system_id)
141
+ updates = flat.select {|k, v| attrs.key?(k) && attrs[k].to_s != v.to_s }
142
+ return if updates.empty?
143
+ sql = "update attributes set value=? where system_id=? and key=?"
144
+ @db.prepare(sql) do |stmt|
145
+ updates.each do |k, v|
146
+ stmt.execute!(v, system_id, k)
147
+ end
148
+ end
149
+ end
150
+
151
+ def insert_attributes(flat, attrs, system_id)
152
+ inserts = flat.select {|k, v| !attrs.key?(k) }
153
+ return if inserts.empty?
154
+ sql = "insert into attributes (system_id, key, value) values (?, ?, ?)"
155
+ @db.prepare(sql) do |stmt|
156
+ inserts.each do |k, v|
157
+ stmt.execute!(system_id, k, v)
158
+ end
159
+ end
160
+ end
161
+
162
+ def database(file)
163
+ SQLite3::Database.new(file).tap do |db|
164
+ db.synchronous = 'off'
165
+ db.execute("create table if not exists systems(id integer primary key, name text not null, os text)")
166
+ db.execute("create index if not exists systems_ix01 on systems (name)")
167
+ db.execute("create table if not exists attributes (system_id integer not null, key text not null, value text)")
168
+ db.execute("create index if not exists attributes_ix01 on attributes (system_id, key)")
169
+ db.execute("create index if not exists attributes_ix02 on attributes (key, value)")
170
+ end
171
+ end
172
+
173
+ def name(doc)
174
+ doc['fqdn'].downcase
175
+ end
176
+
177
+ # Recursively expand a nested Hash into a flat key namespace. For example:
178
+ # flatten({one: {two: {three: 3}}}) #=> {"one.two.three"=>3}
179
+ def flatten(doc, output={}, stack=[])
180
+ case doc
181
+ when Hash
182
+ doc.each do |k,v|
183
+ stack.push(k)
184
+ flatten(v, output, stack)
185
+ stack.pop
186
+ end
187
+ else
188
+ val = doc.is_a?(Array) ? doc.join(',') : doc
189
+ output[stack.join('.')] = val
190
+ end
191
+ output
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,94 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+
6
+ # Behaves just like EM::Queue with the exception that items added to the queue
7
+ # are sorted and popped from the queue by their priority. The optional comparator
8
+ # block passed in the constructor determines element priority, with items sorted
9
+ # highest to lowest. If no block is provided, the elements' natural ordering,
10
+ # via <=>, is used.
11
+ #
12
+ # @example
13
+ #
14
+ # q = PriorityQueue.new do |a, b|
15
+ # a[:priority] <=> b[:priority]
16
+ # end
17
+ # q.push({:priority => 1, :msg => 'one'})
18
+ # q.push({:priority => 2, :msg => 'two'})
19
+ # q.push({:priority => 3, :msg => 'three'})
20
+ # 3.times do
21
+ # q.pop {|item| puts item[:msg] }
22
+ # end
23
+ # #=> "three"
24
+ # # "two"
25
+ # # "one"
26
+ #
27
+ class PriorityQueue < EM::Queue
28
+ def initialize(&comparator)
29
+ super
30
+ @items = Heap.new(&comparator)
31
+ end
32
+
33
+ # A binary max heap implementation for efficient storage of queue items. This
34
+ # class implements the Array methods called by EM::Queue so that it may
35
+ # replace the +@items+ instance variable. Namely, +push+ +shift+, +size+, and
36
+ # +empty?+ are implemented.
37
+ class Heap
38
+ def initialize(*items, &comp)
39
+ @heap = []
40
+ @comp = comp || proc {|a, b| a <=> b }
41
+ push(*items)
42
+ end
43
+
44
+ def push(*items)
45
+ items.flatten.each do |item|
46
+ @heap << item
47
+ move_up(@heap.size - 1)
48
+ end
49
+ end
50
+ alias :<< :push
51
+
52
+ def pop
53
+ return if @heap.empty?
54
+ root = @heap[0]
55
+ @heap[0] = @heap[-1]
56
+ @heap.pop
57
+ move_down(0)
58
+ root
59
+ end
60
+ alias :shift :pop
61
+
62
+ def size
63
+ @heap.size
64
+ end
65
+
66
+ def empty?
67
+ @heap.empty?
68
+ end
69
+
70
+ private
71
+
72
+ def move_down(k)
73
+ left = 2 * k + 1
74
+ right = 2 * k + 2
75
+ return if left > (@heap.size - 1)
76
+ larger = (right < @heap.size && @comp[@heap[right], @heap[left]] > 0) ? right : left
77
+ if @comp[@heap[k], @heap[larger]] < 0
78
+ @heap[k], @heap[larger] = @heap[larger], @heap[k]
79
+ move_down(larger)
80
+ end
81
+ end
82
+
83
+ def move_up(k)
84
+ return if k == 0
85
+ parent = (k - 1) / 2
86
+ if @comp[@heap[k], @heap[parent]] > 0
87
+ @heap[k], @heap[parent] = @heap[parent], @heap[k]
88
+ move_up(parent)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end