motel 0.3 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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