motel 0.3 → 0.3.1

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.
@@ -7,6 +7,8 @@ require 'motel/movement_strategy'
7
7
 
8
8
  module Motel
9
9
 
10
+ # FIXME Motel locations need concurrent access protection, add here (?)
11
+
10
12
  # A Location defines an optional parent location and the x,y,z
11
13
  # cartesian coordinates of the location relative to that parent.
12
14
  # If parent is not specified x,y,z are ignored and this location
@@ -27,6 +29,9 @@ class Location
27
29
  # array of callbacks to be invoked on movement
28
30
  attr_accessor :movement_callbacks
29
31
 
32
+ # Array of callbacks to be invoked on proximity
33
+ attr_accessor :proximity_callbacks
34
+
30
35
  # a generic association which this location can belong to
31
36
  attr_accessor :entity
32
37
 
@@ -34,8 +39,13 @@ class Location
34
39
  # default to the stopped movement strategy
35
40
  @movement_strategy = MovementStrategies::Stopped.instance
36
41
  @movement_callbacks = []
42
+ @proximity_callbacks = []
37
43
  @children = []
38
44
 
45
+ @x = nil
46
+ @y = nil
47
+ @z = nil
48
+
39
49
  @id = args[:id] if args.has_key? :id
40
50
  @parent_id = args[:parent_id] if args.has_key? :parent_id
41
51
  @x = args[:x] if args.has_key? :x
@@ -44,10 +54,6 @@ class Location
44
54
  @parent = args[:parent] if args.has_key? :parent
45
55
  @parent.children.push self unless @parent.nil? || @parent.children.include?(self)
46
56
  @movement_strategy = args[:movement_strategy] if args.has_key? :movement_strategy
47
-
48
- @x = 0 if @x.nil?
49
- @y = 0 if @y.nil?
50
- @z = 0 if @z.nil?
51
57
  end
52
58
 
53
59
  # update this location's attributes to match other's set attributes
@@ -60,6 +66,11 @@ class Location
60
66
  @parent_id = location.parent_id unless location.parent_id.nil?
61
67
  end
62
68
 
69
+ # return this locations coordinates in an array
70
+ def coordinates
71
+ [@x, @y, @z]
72
+ end
73
+
63
74
  # return this location's root location
64
75
  def root
65
76
  return self if parent.nil?
@@ -95,6 +106,14 @@ class Location
95
106
  return parent.total_z + z
96
107
  end
97
108
 
109
+ # return the distance between this location and specified other
110
+ def -(location)
111
+ dx = x - location.x
112
+ dy = y - location.y
113
+ dz = z - location.z
114
+ Math.sqrt(dx ** 2 + dy ** 2 + dz ** 2)
115
+ end
116
+
98
117
  end
99
118
 
100
119
  end # module Motel
@@ -46,6 +46,7 @@ class Elliptical < MovementStrategy
46
46
  @direction_minor_x = 0 if @direction_minor_x.nil?
47
47
  @direction_minor_y = 1 if @direction_minor_y.nil?
48
48
  @direction_minor_z = 0 if @direction_minor_z.nil?
49
+ super(args)
49
50
 
50
51
  @direction_major_x, @direction_major_y, @direction_major_z =
51
52
  normalize(@direction_major_x, @direction_major_y, @direction_major_z)
@@ -23,6 +23,7 @@ class Linear < MovementStrategy
23
23
  @direction_vector_y = args[:direction_vector_y] if args.has_key? :direction_vector_y
24
24
  @direction_vector_z = args[:direction_vector_z] if args.has_key? :direction_vector_z
25
25
  @speed = args[:speed] if args.has_key? :speed
26
+ super(args)
26
27
 
27
28
  # normalize direction vector
28
29
  @direction_vector_x, @direction_vector_y, @direction_vector_z =
@@ -11,13 +11,14 @@ module Motel
11
11
  # MovementStrategy subclasses define the rules and params which
12
12
  # a location changes its position.
13
13
  class MovementStrategy
14
+ attr_accessor :id, :type
15
+
14
16
  attr_accessor :step_delay
15
17
 
16
18
  def initialize(args = {})
17
- @step_delay = 5
18
- @movement_callbacks = []
19
+ @step_delay = 1
19
20
 
20
- @step_delay = args[:step_delay] if args.has_key? :step_delay
21
+ @step_delay = args[:step_delay] if args.has_key?(:step_delay) && !args[:step_delay].nil?
21
22
  end
22
23
 
23
24
  # default movement strategy is to do nothing
@@ -6,7 +6,6 @@
6
6
  # Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
7
7
 
8
8
  require 'singleton'
9
- require 'motel/thread_pool'
10
9
 
11
10
  module Motel
12
11
 
@@ -17,44 +16,79 @@ module Motel
17
16
  class Runner
18
17
  include Singleton
19
18
 
20
- # locations being managed
21
- attr_accessor :locations
22
-
23
- # for testing purposes
24
- attr_reader :thread_pool, :terminate, :run_thread
19
+ # For testing purposes
20
+ attr_reader :terminate, :run_thread
25
21
 
26
22
  def initialize(args = {})
23
+ # is set to true upon runner termination
27
24
  @terminate = false
28
- @locations = []
29
- @locations_lock = Mutex.new
25
+
26
+ # TODO use ruby tree to store locations w/ heirarchy
27
+ # management queues, locations to be scheduled and locations to be run
28
+ @schedule_queue = []
29
+ @run_queue = []
30
+
31
+
32
+ # locks protecting queues from concurrent access and conditions indicating queues have items
33
+ @schedule_lock = Mutex.new
34
+ @run_lock = Mutex.new
35
+ @schedule_cv = ConditionVariable.new
36
+ @run_cv = ConditionVariable.new
30
37
 
31
38
  @run_thread = nil
32
- @run_delay = 2 # FIXME scale delay (only needed if locations is empty or has very few simple elements)
33
39
  end
34
40
 
41
+ # Return complete list of locations being managed/tracked
42
+ def locations
43
+ # need conccurrent protection here, or copy the elements into another array and return that?
44
+ @schedule_queue + @run_queue
45
+ end
46
+
47
+
35
48
  # Empty the list of locations being managed/tracked
36
49
  def clear
37
- @locations_lock.synchronize {
38
- @locations.clear
39
- }
50
+ @schedule_lock.synchronize {
51
+ @run_lock.synchronize {
52
+ @schedule_queue.clear
53
+ @run_queue.clear
54
+ }}
40
55
  end
41
56
 
42
- # add location to runner to be managed, after this is called, the location's
57
+ # Add location to runner to be managed, after this is called, the location's
43
58
  # movement strategy's move method will be invoked periodically
44
59
  def run(location)
45
- @locations_lock.synchronize {
46
- Logger.debug "adding location #{location.id} to run queue"
47
- @locations.push location
60
+ @schedule_lock.synchronize {
61
+ # autogenerate location.id if nil
62
+ if location.id.nil?
63
+ @run_lock.synchronize {
64
+ i = 1
65
+ until false
66
+ break if @schedule_queue.find { |l| l.id == i }.nil? && @run_queue.find { |l| l.id == i }.nil?
67
+ i += 1
68
+ end
69
+ location.id = i
70
+ }
71
+ end
72
+
73
+ Logger.debug "adding location #{location.id} to runner queue"
74
+ @schedule_queue.push location
75
+ @schedule_cv.signal
48
76
  }
77
+ return location
78
+ end
79
+
80
+ # Wrapper around run, except return 'self' when done
81
+ def <<(location)
82
+ run(location)
83
+ return self
49
84
  end
50
85
 
51
86
  # Start moving the locations. If :async => true is passed in, this will immediately
52
87
  # return, else this will block until stop is called.
53
88
  def start(args = {})
54
- num_threads = 5
55
- num_threads = args[:num_threads] if args.has_key? :num_threads
89
+ @num_threads = 5
90
+ @num_threads = args[:num_threads] if args.has_key? :num_threads
56
91
  @terminate = false
57
- @thread_pool = ThreadPool.new(num_threads)
58
92
 
59
93
  if args.has_key?(:async) && args[:async]
60
94
  Logger.debug "starting async motel runner"
@@ -70,7 +104,12 @@ class Runner
70
104
  def stop
71
105
  Logger.debug "stopping motel runner"
72
106
  @terminate = true
73
- @thread_pool.shutdown
107
+ @schedule_lock.synchronize {
108
+ @schedule_cv.signal
109
+ }
110
+ @run_lock.synchronize {
111
+ @run_cv.signal
112
+ }
74
113
  join
75
114
  Logger.debug "motel runner stopped"
76
115
  end
@@ -85,35 +124,94 @@ class Runner
85
124
 
86
125
  # Internal helper method performing main runner operations
87
126
  def run_cycle
88
- # track time between runs
89
- start_time = Time.now
127
+ # location ids which are currently being run -> their run timestamp
128
+ location_timestamps = {}
129
+
130
+ # scheduler thread, to add locations to the run queue
131
+ scheduler = Thread.new {
132
+ until @terminate
133
+ tqueue = []
134
+ locs_to_run = []
135
+ empty_queue = true
136
+ min_delay = nil
137
+
138
+ @schedule_lock.synchronize {
139
+ # if no locations are to be scheduled, block until there are
140
+ @schedule_cv.wait(@schedule_lock) if @schedule_queue.empty?
141
+ @schedule_queue.each { |l| tqueue << l }
142
+ }
143
+
144
+ # run through each location to be scheduled to run, see which ones are due
145
+ tqueue.each { |loc|
146
+ location_timestamps[loc.id] = Time.now unless location_timestamps.has_key?(loc.id)
147
+ locs_to_run << loc if loc.movement_strategy.step_delay < Time.now - location_timestamps[loc.id]
148
+ }
149
+
150
+ # add those the the run queue, signal runner to start operations if blocking
151
+ @schedule_lock.synchronize {
152
+ @run_lock.synchronize{
153
+ locs_to_run.each { |loc| @run_queue << loc ; @schedule_queue.delete(loc) }
154
+ empty_queue = (@schedule_queue.size == 0)
155
+ @run_cv.signal unless locs_to_run.empty?
156
+ }
157
+ }
90
158
 
159
+ # if there are locations still to be scheduled, sleep for the smallest step_delay
160
+ unless empty_queue
161
+ # we use locations instead of @schedule_queue here since a when the scheduler is
162
+ # sleeping a loc w/ a smaller step_delay may complete running and be added back to the scheduler
163
+ min_delay= locations.sort { |a,b|
164
+ a.movement_strategy.step_delay <=> b.movement_strategy.step_delay
165
+ }.first.movement_strategy.step_delay
166
+ sleep min_delay
167
+ end
168
+ end
169
+ }
170
+
171
+ # until we are told to stop
91
172
  until @terminate
92
- # copy locations into temp 2nd array so we're not holding up lock on locations array
93
- tlocations = []
94
- @locations_lock.synchronize {
95
- @locations.each { |loc| tlocations.push loc }
173
+ locs_to_schedule = []
174
+ tqueue = []
175
+
176
+ @run_lock.synchronize{
177
+ # wait until we have locations to run
178
+ @run_cv.wait(@run_lock) if @run_queue.empty?
179
+ @run_queue.each { |l| tqueue << l }
96
180
  }
97
181
 
98
- tlocations.each { |loc|
99
- @thread_pool.dispatch(loc) { |loc|
100
- Logger.debug "runner moving location #{loc.id} via #{loc.movement_strategy.class.to_s}"
182
+ # run through each location to be run, perform actual movement, invoke callbacks
183
+ tqueue.each { |loc|
184
+ Logger.debug "runner moving location #{loc.id} at #{loc.coordinates.join(",")} via #{loc.movement_strategy.class.to_s}"
101
185
 
102
- loc.movement_strategy.move loc, start_time - Time.now
103
- start_time = Time.now
186
+ # store the old location coordinates for comparison after the movement
187
+ old_coords = [loc.x, loc.y, loc.z]
104
188
 
105
- # TODO see if loc coordinates changed b4 doing this
106
- loc.movement_callbacks.each { |callback|
107
- callback.call(loc)
108
- }
189
+ elapsed = Time.now - location_timestamps[loc.id]
190
+ loc.movement_strategy.move loc, elapsed
191
+ location_timestamps[loc.id] = Time.now
109
192
 
110
- ## delay as long as the strategy tells us to
111
- sleep loc.movement_strategy.step_delay
193
+ # TODO invoke these async so as not to hold up the runner
194
+ # make sure to keep these in sync w/ those invoked in the simrpc adapter "update_location" handler
195
+ loc.movement_callbacks.each { |callback|
196
+ callback.invoke(loc, *old_coords)
112
197
  }
198
+ loc.proximity_callbacks.each { |callback|
199
+ callback.invoke(loc)
200
+ }
201
+
202
+ locs_to_schedule << loc
113
203
  }
114
204
 
115
- sleep @run_delay
205
+ # add locations back to schedule queue
206
+ @run_lock.synchronize{
207
+ @schedule_lock.synchronize{
208
+ locs_to_schedule.each { |loc| @schedule_queue << loc ; @run_queue.delete(loc) }
209
+ @schedule_cv.signal unless locs_to_schedule.empty?
210
+ }
211
+ }
116
212
  end
213
+
214
+ scheduler.join
117
215
  end
118
216
 
119
217
  end
@@ -0,0 +1,190 @@
1
+ # Motel simrpc adapter
2
+ #
3
+ # Copyright (C) 2010 Mohammed Morsi <movitto@yahoo.com>
4
+ # Licensed under the AGPLv3+ http://www.gnu.org/licenses/agpl.txt
5
+
6
+ require 'simrpc'
7
+
8
+ module Motel
9
+
10
+ # Motel::Server defines a server endpoint which manages locations
11
+ # and responds to simrpc requests
12
+ class Server
13
+ def initialize(args = {})
14
+ simrpc_args = args
15
+ simrpc_args[:id] = "location-server"
16
+
17
+ # create a simprc node
18
+ @simrpc_node = Simrpc::Node.new(simrpc_args)
19
+
20
+ # register handlers for the various motel simrpc methods
21
+ @simrpc_node.handle_method("get_location") { |location_id|
22
+ Logger.info "received get location #{location_id} request"
23
+ loc = nil
24
+ begin
25
+ loc = Runner.instance.locations.find { |loc| loc.id == location_id }
26
+ # FIXME traverse all of loc's descendants, and if remote location
27
+ # server is specified, send request to get child location, swapping
28
+ # it in for the one thats there
29
+ rescue Exception => e
30
+ Logger.warn "get location #{location_id} failed w/ exception #{e}"
31
+ end
32
+ Logger.info "get location #{location_id} request returning #{loc}"
33
+ loc
34
+ }
35
+
36
+ @simrpc_node.handle_method("create_location") { |location|
37
+ Logger.info "received create location request"
38
+ location = Location.new if location.nil?
39
+ ret = location
40
+ begin
41
+ location.x = 0 if location.x.nil?
42
+ location.y = 0 if location.y.nil?
43
+ location.z = 0 if location.z.nil?
44
+
45
+ # TODO decendants support w/ remote option (create additional locations on other servers)
46
+ Runner.instance.run location
47
+
48
+ rescue Exception => e
49
+ Logger.warn "create location failed w/ exception #{e}"
50
+ ret = nil
51
+ end
52
+ Logger.info "create location request created and returning #{ret.id}"
53
+ ret
54
+ }
55
+
56
+ @simrpc_node.handle_method("update_location") { |location|
57
+ Logger.info "received update location #{location.id} request"
58
+ success = true
59
+ if location.nil?
60
+ success = false
61
+ else
62
+ rloc = Runner.instance.locations.find { |loc| loc.id == location.id }
63
+ begin
64
+ # store the old location coordinates for comparison after the movement
65
+ old_coords = [location.x, location.y, location.z]
66
+
67
+ # FIXME XXX big problem/bug here, client must always specify location.movement_strategy, else location constructor will set it to stopped
68
+ # FIXME this should halt location movement, update location, then start it again
69
+ Logger.info "updating location #{location.id} with #{location}/#{location.movement_strategy}"
70
+ rloc.update(location)
71
+
72
+ # FIXME trigger location movement & proximity callbacks (make sure to keep these in sync w/ those invoked the the runner)
73
+ # right now we can't do this because a single simrpc node can't handle multiple sent message response, see FIXME XXX in lib/simrpc/node.rb
74
+ #rloc.movement_callbacks.each { |callback|
75
+ # callback.invoke(rloc, *old_coords)
76
+ #}
77
+ #rloc.proximity_callbacks.each { |callback|
78
+ # callback.invoke(rloc)
79
+ #}
80
+
81
+ rescue Exception => e
82
+ Logger.warn "update location #{location.id} failed w/ exception #{e}"
83
+ success = false
84
+ end
85
+ end
86
+ Logger.info "update location #{location.id} returning #{success}"
87
+ success
88
+ }
89
+
90
+ @simrpc_node.handle_method("subscribe_to_location_movement") { |client_id, location_id, min_distance, min_x, min_y, min_z|
91
+ Logger.info "subscribe client #{client_id} to location #{location_id} movement request received"
92
+ loc = Runner.instance.locations.find { |loc| loc.id == location_id }
93
+ success = true
94
+ if loc.nil?
95
+ success = false
96
+ else
97
+ callback = Callbacks::Movement.new :min_distance => min_distance, :min_x => min_x, :min_y => min_y, :min_z => min_z,
98
+ :handler => lambda { |location, d, dx, dy, dz|
99
+ # send location to client
100
+ @simrpc_node.send_method("location_moved", client_id, location, d, dx, dy, dz)
101
+ }
102
+ loc.movement_callbacks.push callback
103
+ end
104
+ Logger.info "subscribe client #{client_id} to location #{location_id} movement returning #{success}"
105
+ success
106
+ }
107
+
108
+ @simrpc_node.handle_method("subscribe_to_locations_proximity") { |client_id, location1_id, location2_id, event, max_distance, max_x, max_y, max_z|
109
+ Logger.info "subscribe client #{client_id} to location #{location1_id}/#{location2_id} proximity request received"
110
+ loc1 = Runner.instance.locations.find { |loc| loc.id == location1_id }
111
+ loc2 = Runner.instance.locations.find { |loc| loc.id == location2_id }
112
+ success = true
113
+ if loc1.nil? || loc2.nil?
114
+ success = false
115
+ else
116
+ callback = Callbacks::Proximity.new :to_location => loc2, :event => event, :max_distance => max_distance, :max_x => max_x, :max_y => max_y, :max_z => max_z,
117
+ :handler => lambda { |location1, location2|
118
+ # send locations to client
119
+ @simrpc_node.send_method("locations_proximity", client_id, location1, location2)
120
+ }
121
+ loc1.proximity_callbacks.push callback
122
+ end
123
+ Logger.info "subscribe client #{client_id} to location #{location1_id}/#{location2_id} proximity request returning #{success}"
124
+ success
125
+ }
126
+ end
127
+
128
+ def terminate
129
+ @simrpc_node.terminate
130
+ end
131
+
132
+ def join
133
+ @simrpc_node.join
134
+ end
135
+ end
136
+
137
+ # Client defines a client endpoint that performs
138
+ # a request against a Motel Server
139
+ class Client
140
+ # Set to a callable object that will take a location and distance moved
141
+ attr_writer :on_location_moved
142
+
143
+ # Set to a callable object that will take two locations
144
+ attr_writer :on_locations_proximity
145
+
146
+ # Initialize the client with various args, all of which are passed onto Simrpc::Node constructor
147
+ def initialize(args = {})
148
+ simrpc_args = args
149
+ simrpc_args[:destination] = "location-server"
150
+
151
+ @simrpc_node = Simrpc::Node.new(simrpc_args)
152
+ end
153
+
154
+ def join
155
+ @simrpc_node.join
156
+ end
157
+
158
+ def request(target, *args)
159
+ method_missing(target, *args)
160
+ end
161
+
162
+ # pass simrpc method requests right onto the simrpc node
163
+ def method_missing(method_id, *args)
164
+ # special case for subsscribe_to_location,
165
+ if method_id == :subscribe_to_location_movement
166
+ # add simrpc node id onto args list
167
+ args.unshift @simrpc_node.id
168
+
169
+ # handle location updates from the server, & issue subscribe request
170
+ @simrpc_node.handle_method("location_moved") { |location, d, dx, dy, dz|
171
+ Logger.info "location #{location.id} moved"
172
+ @on_location_moved.call(location, d, dx, dy, dz) unless @on_location_moved.nil?
173
+ }
174
+
175
+ elsif method_id == :subscribe_to_locations_proximity
176
+ # add simrpc node id onto args list
177
+ args.unshift @simrpc_node.id
178
+
179
+ # handle location proximity events from the server, & issue subscribe request
180
+ @simrpc_node.handle_method("locations_proximity") { |location1, location2|
181
+ Logger.info "location #{location1.id}/#{location2.id} proximity"
182
+ @on_locations_proximity.call(location1, location2) unless @on_locations_proximity.nil?
183
+ }
184
+ end
185
+
186
+ @simrpc_node.method_missing(method_id, *args)
187
+ end
188
+ end
189
+
190
+ end