archipelago 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -20,15 +20,15 @@ placed within the pkg/ directory.
20
20
 
21
21
  To set up an Archipelago::Tranny::Manager do the following (from scripts/tranny.rb):
22
22
 
23
- :include:scripts/tranny.rb
23
+ :include:script/tranny.rb
24
24
 
25
25
  To set up an Archipelago::Treasure::Chest do the following (from scripts/chest.rb):
26
26
 
27
- :include:scripts/chest.rb
27
+ :include:script/chest.rb
28
28
 
29
29
  To set up an Archipelago::Pirate::Captain do the following (from scripts/pirate.rb):
30
30
 
31
- :include:scripts/pirate.rb
31
+ :include:script/pirate.rb
32
32
 
33
33
  To set up a test environment to play around with, run the following
34
34
  commands in a few terminals:
@@ -33,6 +33,75 @@ module Archipelago
33
33
 
34
34
  module Current
35
35
 
36
+ #
37
+ # Adds a few threaded methods to the normal ruby collections.
38
+ #
39
+ # NB: Will work slightly different than the unthreaded ones in certain circumstances.
40
+ #
41
+ module ThreadedCollection
42
+
43
+ #
44
+ # Like each, except calls +block+ within a new thread.
45
+ #
46
+ def t_each(&block)
47
+ threads = []
48
+ self.each do |args|
49
+ threads << Thread.new do
50
+ yield(args)
51
+ end
52
+ end
53
+ threads.each do |thread|
54
+ thread.join
55
+ end
56
+ end
57
+
58
+ #
59
+ # Like collect, except calls +block+ within a new thread.
60
+ #
61
+ def t_collect(&block)
62
+ result = []
63
+ result.extend(Synchronized)
64
+ self.t_each do |args|
65
+ result.synchronize do
66
+ result << yield(args)
67
+ end
68
+ end
69
+ return result
70
+ end
71
+
72
+ #
73
+ # Like select, except calls +block+ within a new thread.
74
+ #
75
+ def t_select(&block)
76
+ result = []
77
+ result.extend(Synchronized)
78
+ self.t_each do |args|
79
+ matches = yield(args)
80
+ result.synchronize do
81
+ result << args
82
+ end if matches
83
+ end
84
+ return result
85
+ end
86
+
87
+ #
88
+ # Like reject, except calls +block+ within a new thread.
89
+ #
90
+ def t_reject(&block)
91
+ result = []
92
+ result.extend(Synchronized)
93
+ self.t_each do |args|
94
+ matches = yield(args)
95
+ result.synchronize do
96
+ result << args
97
+ end unless matches
98
+ end
99
+ return result
100
+ end
101
+
102
+ end
103
+
104
+
36
105
  #
37
106
  # A module that will allow any class to synchronize over any other
38
107
  # object.
@@ -66,6 +66,11 @@ module Archipelago
66
66
  #
67
67
  THRIFTY_PUBLISHING = false
68
68
 
69
+ #
70
+ # The host we are running on.
71
+ #
72
+ HOST = "#{Socket::gethostbyname(Socket::gethostname)[0]}" rescue "localhost"
73
+
69
74
  #
70
75
  # A module to simplify publishing services.
71
76
  #
@@ -138,7 +143,25 @@ module Archipelago
138
143
  # We are always valid if we are able to reply.
139
144
  #
140
145
  def valid?
141
- true
146
+ if defined?(@valid)
147
+ @valid
148
+ else
149
+ @valid = true
150
+ end
151
+ end
152
+
153
+ #
154
+ # Stops the publishing of this Publishable.
155
+ #
156
+ def stop!
157
+ if valid?
158
+ @valid = false
159
+ if defined?(Archipelago::Disco::MC) && @jockey == Archipelago::Disco::MC
160
+ @jockey.unpublish(self.service_id)
161
+ else
162
+ @jockey.stop!
163
+ end
164
+ end
142
165
  end
143
166
 
144
167
  #
@@ -153,12 +176,7 @@ module Archipelago
153
176
  # Stuff that didnt fit in any of the other databases.
154
177
  #
155
178
  @metadata ||= @persistence_provider.get_hashish("metadata")
156
- service_id = @metadata["service_id"]
157
- unless service_id
158
- host = "#{Socket::gethostbyname(Socket::gethostname)[0]}" rescue "localhost"
159
- service_id = @metadata["service_id"] ||= Digest::SHA1.hexdigest("#{host}:#{Time.new.to_f}:#{self.object_id}:#{rand(1 << 32)}")
160
- end
161
- return service_id
179
+ return @metadata["service_id"] ||= Digest::SHA1.hexdigest("#{HOST}:#{Time.new.to_f}:#{self.object_id}:#{rand(1 << 32)}").to_s
162
180
  end
163
181
 
164
182
  end
@@ -219,6 +237,16 @@ module Archipelago
219
237
  class Query < ServiceDescription
220
238
  end
221
239
 
240
+ #
241
+ # A class used to defined removed services.
242
+ #
243
+ class UnPublish
244
+ attr_reader :service_id
245
+ def initialize(service_id)
246
+ @service_id = service_id
247
+ end
248
+ end
249
+
222
250
  #
223
251
  # A class used to define an existing service.
224
252
  #
@@ -249,6 +277,7 @@ module Archipelago
249
277
  class ServiceLocker
250
278
  attr_reader :hash
251
279
  include Archipelago::Current::Synchronized
280
+ include Archipelago::Current::ThreadedCollection
252
281
  def initialize(hash = nil)
253
282
  super
254
283
  @hash = hash || {}
@@ -274,7 +303,7 @@ module Archipelago
274
303
  end
275
304
  end
276
305
  else
277
- super(*args)
306
+ super(meth, *args, &block)
278
307
  end
279
308
  end
280
309
  #
@@ -325,21 +354,35 @@ module Archipelago
325
354
  # or if not given THRIFTY_REPLYING. Otherwise will reply with multicasts.
326
355
  #
327
356
  def initialize(options = {})
328
- @thrifty_caching = options.include?(:thrifty_caching) ? options[:thrifty_caching] : THRIFTY_CACHING
329
- @thrifty_replying = options.include?(:thrifty_replying) ? options[:thrifty_replying] : THRIFTY_REPLYING
330
- @thrifty_publishing = options.include?(:thrifty_publishing) ? options[:thrifty_publishing] : THRIFTY_PUBLISHING
331
- @lookup_timeout = options[:lookup_timeout] || LOOKUP_TIMEOUT
332
- @initial_lookup_standoff = options[:initial_lookup_standoff] || INITIAL_LOOKUP_STANDOFF
333
-
357
+ @valid = true
334
358
  @remote_services = ServiceLocker.new
335
359
  @local_services = ServiceLocker.new
336
360
  @subscribed_services = Set.new
337
361
 
338
362
  @incoming = Queue.new
339
363
  @outgoing = Queue.new
340
-
364
+
341
365
  @new_service_semaphore = MonitorMixin::ConditionVariable.new(Archipelago::Current::Lock.new)
342
-
366
+
367
+ setup(options)
368
+
369
+ start_listener
370
+ start_unilistener
371
+ start_shouter
372
+ start_picker
373
+ start_validator(options[:validation_interval] || VALIDATION_INTERVAL)
374
+ end
375
+
376
+ #
377
+ # Sets up this instance according to the given +options+.
378
+ #
379
+ def setup(options = {})
380
+ @thrifty_caching = options.include?(:thrifty_caching) ? options[:thrifty_caching] : THRIFTY_CACHING
381
+ @thrifty_replying = options.include?(:thrifty_replying) ? options[:thrifty_replying] : THRIFTY_REPLYING
382
+ @thrifty_publishing = options.include?(:thrifty_publishing) ? options[:thrifty_publishing] : THRIFTY_PUBLISHING
383
+ @lookup_timeout = options[:lookup_timeout] || LOOKUP_TIMEOUT
384
+ @initial_lookup_standoff = options[:initial_lookup_standoff] || INITIAL_LOOKUP_STANDOFF
385
+
343
386
  @listener = UDPSocket.new
344
387
  @unilistener = UDPSocket.new
345
388
 
@@ -371,29 +414,40 @@ module Archipelago
371
414
  raise e
372
415
  end
373
416
  end
374
- @unicast_address = "#{Socket::gethostbyname(Socket::gethostname)[0]}:#{this_port}" rescue "localhost:#{this_port}"
417
+ @unicast_address = "#{HOST}:#{this_port}"
375
418
 
376
419
  @sender = UDPSocket.new
377
420
  @sender.connect(options[:address] || ADDRESS, options[:port] || PORT)
378
421
 
379
422
  @unisender = UDPSocket.new
423
+ end
380
424
 
381
- start_listener
382
- start_unilistener
383
- start_shouter
384
- start_picker
385
- start_validator(options[:validation_interval] || VALIDATION_INTERVAL)
425
+ #
426
+ # Clears our local and remote services.
427
+ #
428
+ def clear!
429
+ @local_services = ServiceLocker.new
430
+ @remote_services = ServiceLocker.new
386
431
  end
387
432
 
388
433
  #
389
434
  # Stops all the threads in this instance.
390
435
  #
391
436
  def stop!
392
- @listener_thread.kill
393
- @unilistener_thread.kill
394
- @shouter_thread.kill
395
- @picker_thread.kill
396
- @validator_thread.kill
437
+ if @valid
438
+ @valid = false
439
+ @local_services.each do |service_id, service_description|
440
+ self.unpublish(service_id)
441
+ end
442
+ @listener_thread.kill
443
+ @unilistener_thread.kill
444
+ @validator_thread.kill
445
+ @picker_thread.kill
446
+ until @outgoing.empty?
447
+ sleep(0.01)
448
+ end
449
+ @shouter_thread.kill
450
+ end
397
451
  end
398
452
 
399
453
  #
@@ -443,6 +497,17 @@ module Archipelago
443
497
  end
444
498
  end
445
499
 
500
+ #
501
+ # Removes the service with given +service_id+ from the published services.
502
+ #
503
+ def unpublish(service_id)
504
+ @local_services.delete(service_id)
505
+ @new_service_semaphore.broadcast
506
+ unless @thrifty_publishing
507
+ @outgoing << [nil, UnPublish.new(service_id)]
508
+ end
509
+ end
510
+
446
511
  private
447
512
 
448
513
  #
@@ -523,8 +588,8 @@ module Archipelago
523
588
  end
524
589
 
525
590
  #
526
- # Start the thread picking incoming Records and Queries and
527
- # handling them properly
591
+ # Start the thread picking incoming Records, Queries and UnPublishes and
592
+ # handling them properly.
528
593
  #
529
594
  def start_picker
530
595
  @picker_thread = Thread.new do
@@ -544,6 +609,8 @@ module Archipelago
544
609
  @remote_services[data[:service_id]] = data
545
610
  @new_service_semaphore.broadcast
546
611
  end
612
+ elsif Archipelago::Disco::UnPublish === data
613
+ @remote_services.delete(data.service_id)
547
614
  end
548
615
  rescue Exception => e
549
616
  puts e
@@ -566,6 +633,12 @@ module Archipelago
566
633
 
567
634
  end
568
635
 
636
+ #
637
+ # The default Archipelago::Disco::Jockey that is always available for lookups is Archipelago::Disco::MC.
638
+ #
639
+ # If you really need to you can customize it by defining MC_OPTIONS before loading disco.rb, and if you REALLY
640
+ # need to you can disable it completely by setting MC_DISABLED to true.
641
+ #
569
642
  MC = Jockey.new(defined?(MC_OPTIONS) ? MC_OPTIONS : {}) unless defined?(MC_DISABLED) && MC_DISABLED
570
643
 
571
644
  end
@@ -48,6 +48,13 @@ module Archipelago
48
48
  @lock = Archipelago::Current::Lock.new
49
49
  end
50
50
  #
51
+ # Close the @content_db and @timestamps_db behind this BerkeleyHashish
52
+ #
53
+ def close!
54
+ @content_db.close
55
+ @timestamps_db.close
56
+ end
57
+ #
51
58
  # Returns a deep ( Marshal.load(Marshal.dump(o)) ) clone
52
59
  # of the object represented by +key+.
53
60
  #
@@ -70,12 +77,16 @@ module Archipelago
70
77
  #
71
78
  # Insert +value+ under +key+.
72
79
  #
80
+ # Will call <b>value.save_hook(old_value)</b> and send
81
+ # it a block that does the actual saving if
82
+ # <b>value.respond_to?(:save_hook)</b>.
83
+ #
73
84
  def []=(key, value)
74
85
  @lock.synchronize_on(key) do
75
86
 
76
87
  @content[key] = value
77
88
 
78
- write_to_db(key, Marshal.dump(key), Marshal.dump(value))
89
+ write_to_db(key, Marshal.dump(key), Marshal.dump(value), value)
79
90
 
80
91
  return value
81
92
 
@@ -85,13 +96,19 @@ module Archipelago
85
96
  # Stores whatever is under +key+ if it is not the same as
86
97
  # whats in the persistent db.
87
98
  #
99
+ # Will call <b>value.save_hook(old_value)</b> and send
100
+ # it a block that does the actual saving if
101
+ # <b>value.respond_to?(:save_hook)</b> and
102
+ # a save is actually performed.
103
+ #
88
104
  def store_if_changed(key)
89
105
  @lock.synchronize_on(key) do
90
106
 
91
107
  serialized_key = Marshal.dump(key)
92
- serialized_value = Marshal.dump(@content[key])
108
+ value = @content[key]
109
+ serialized_value = Marshal.dump(value)
93
110
 
94
- write_to_db(key, serialized_key, serialized_value) if @content_db[serialized_key] != serialized_value
111
+ write_to_db(key, serialized_key, serialized_value, value) if @content_db[serialized_key] != serialized_value
95
112
 
96
113
  end
97
114
  end
@@ -115,6 +132,19 @@ module Archipelago
115
132
  end
116
133
  end
117
134
  #
135
+ # Will do +callable+.call(key, value) for each
136
+ # key-and-value pair in this Hashish.
137
+ #
138
+ # NB: This is totaly thread-unsafe, only do this
139
+ # for management or rescue!
140
+ #
141
+ def each(callable)
142
+ @content_db.each do |serialized_key, serialized_value|
143
+ key = Marshal.load(serialized_key)
144
+ callable.call(key, self.[](key))
145
+ end
146
+ end
147
+ #
118
148
  # Delete +key+ and its value and timestamp.
119
149
  #
120
150
  def delete(key)
@@ -136,7 +166,27 @@ module Archipelago
136
166
  # Write +key+, serialized as +serialized_key+ and
137
167
  # +serialized_value+ to the db.
138
168
  #
139
- def write_to_db(key, serialized_key, serialized_value)
169
+ # Will call <b>value.save_hook(old_value)</b> and send
170
+ # it a block that does the actual saving if
171
+ # <b>value.respond_to?(:save_hook)</b>.
172
+ #
173
+ def write_to_db(key, serialized_key, serialized_value, value)
174
+ if value.respond_to?(:save_hook)
175
+ old_serialized_value = @content_db[serialized_key]
176
+ old_value = old_serialized_value ? Marshal.load(old_serialized_value) : nil
177
+ value.save_hook(old_value) do
178
+ do_write_to_db(key, serialized_key, serialized_value)
179
+ end
180
+ else
181
+ do_write_to_db(key, serialized_key, serialized_value)
182
+ end
183
+ end
184
+
185
+ #
186
+ # Actually writes +key+ serialized as +serialized_key+ an
187
+ # +serialized_value+ to the db. Used by <b>write_to_db</b>.
188
+ #
189
+ def do_write_to_db(key, serialized_key, serialized_value)
140
190
  now = Time.now
141
191
 
142
192
  @content_db[serialized_key] = serialized_value
@@ -170,6 +220,8 @@ module Archipelago
170
220
  def initialize(env_path)
171
221
  env_path.mkpath
172
222
  @env = BDB::Env.open(env_path, BDB::CREATE | BDB::INIT_MPOOL)
223
+ @berkeley_hashishes = []
224
+ @bdb_dbs = []
173
225
  end
174
226
  #
175
227
  # Returns a cleverly cached (but slightly inefficient)
@@ -177,20 +229,31 @@ module Archipelago
177
229
  # using +name+.
178
230
  #
179
231
  def get_cached_hashish(name)
180
- BerkeleyHashish.new(name, @env)
232
+ hashish = BerkeleyHashish.new(name, @env)
233
+ @berkeley_hashishes << hashish
234
+ return hashish
181
235
  end
182
236
  #
183
237
  # Returns a normal hash-like instance using +name+.
184
238
  #
185
239
  def get_hashish(name)
186
- @env.open_db(BDB::HASH, name, nil, BDB::CREATE | BDB::NOMMAP)
240
+ db = @env.open_db(BDB::HASH, name, nil, BDB::CREATE | BDB::NOMMAP)
241
+ @bdb_dbs << db
242
+ return db
187
243
  end
188
244
  #
189
- # Removes the persistent files of this instance.
245
+ # Closes databases opened by this instance and removes the persistent files.
190
246
  #
191
247
  def unlink
192
- p = Pathname.new(@env.home)
193
- p.rmtree if p.exist?
248
+ @berkeley_hashishes.each do |h|
249
+ h.close!
250
+ end
251
+ @bdb_dbs.each do |d|
252
+ d.close
253
+ end
254
+ home = Pathname.new(@env.home)
255
+ @env.close
256
+ home.rmtree if home.exist?
194
257
  end
195
258
  end
196
259
 
@@ -78,22 +78,37 @@ module Archipelago
78
78
  # of required classes and modules at the chest.
79
79
  #
80
80
  def initialize(options = {})
81
- @treasure_map = defined?(Archipelago::Disco::MC) ? Archipelago::Disco::MC : Archipelago::Disco::Jockey.new(options[:jockey_options] || {})
81
+ setup(options)
82
+
83
+ @transaction = nil
84
+
85
+ start_service_updater(options[:initial_service_update_interval] || INITIAL_SERVICE_UPDATE_INTERVAL,
86
+ options[:maximum_service_update_interval] || MAXIMUM_SERVICE_UPDATE_INTERVAL)
87
+
88
+ end
89
+
90
+ #
91
+ # Sets up this instance with the given +options+.
92
+ #
93
+ def setup(options = {})
94
+ @treasure_map ||= nil
95
+ if defined?(Archipelago::Disco::MC)
96
+ @treasure_map.stop! if @treasure_map && @treasure_map != Archipelago::Disco::MC
97
+ @treasure_map = options.include?(:jockey_options) ? Archipelago::Disco::Jockey.new(options[:jockey_options]) : Archipelago::Disco::MC
98
+ else
99
+ @treasure_map.stop! if @treasure_map
100
+ @treasure_map = Archipelago::Disco::Jockey.new(options[:jockey_options] || {})
101
+ end
82
102
 
83
103
  @chest_description = CHEST_DESCRIPTION.merge(options[:chest_description] || {})
84
104
  @tranny_description = TRANNY_DESCRIPTION.merge(options[:tranny_description] || {})
85
- @jockey_options = options[:jockey_options] || {}
86
105
 
87
- @chest_eval_files = options[:chest_eval_files] || []
106
+ @chest_eval_files ||= []
107
+ @chest_eval_files += options[:chest_eval_files] || []
88
108
 
89
- @chests_having_evaluated = {}
90
-
91
- start_service_updater(options[:initial_service_update_interval] || INITIAL_SERVICE_UPDATE_INTERVAL,
92
- options[:maximum_service_update_interval] || MAXIMUM_SERVICE_UPDATE_INTERVAL)
109
+ @chests_having_evaluated ||= {}
93
110
 
94
111
  @yar_counter = 0
95
-
96
- @transaction = nil
97
112
  end
98
113
 
99
114
  #
@@ -199,10 +214,36 @@ module Archipelago
199
214
  end
200
215
 
201
216
  #
202
- # Stops the service update thread for this Pirate.
217
+ # Stops the service update thread for this Pirate and also unpublishes and/or
218
+ # stops the Jockey.
203
219
  #
204
220
  def stop!
205
221
  @service_update_thread.kill
222
+ unless defined?(Archipelago::Disco::MC) && @treasure_map && @treasure_map == Archipelago::Disco::MC
223
+ @treasure_map.stop!
224
+ end
225
+ end
226
+
227
+ #
228
+ # Will do +callable+.call(key, value)
229
+ # for each key-and-value pair in this database network.
230
+ #
231
+ # NB: This is totaly thread-unsafe, only do this
232
+ # for management or rescue!
233
+ #
234
+ def each(callable)
235
+ @chests.t_each do |service_id, chest|
236
+ chest[:service].each(callable)
237
+ end
238
+ end
239
+
240
+ #
241
+ # Does an immediate update of our service lists.
242
+ #
243
+ def update_services!
244
+ @chests = @treasure_map.lookup(Archipelago::Disco::Query.new(@chest_description), 0)
245
+ evaluate_in_chests
246
+ @trannies = @treasure_map.lookup(Archipelago::Disco::Query.new(@tranny_description), 0)
206
247
  end
207
248
 
208
249
  private
@@ -271,9 +312,7 @@ module Archipelago
271
312
  # +initial+ and +maximum+ seconds.
272
313
  #
273
314
  def start_service_updater(initial, maximum)
274
- @chests = @treasure_map.lookup(Archipelago::Disco::Query.new(@chest_description), 0)
275
- evaluate_in_chests
276
- @trannies = @treasure_map.lookup(Archipelago::Disco::Query.new(@tranny_description), 0)
315
+ update_services!
277
316
  @service_update_thread = Thread.start do
278
317
  standoff = initial
279
318
  loop do
@@ -281,9 +320,7 @@ module Archipelago
281
320
  sleep(standoff)
282
321
  standoff *= 2
283
322
  standoff = maximum if standoff > maximum
284
- @chests = @treasure_map.lookup(Archipelago::Disco::Query.new(@chest_description), 0)
285
- evaluate_in_chests
286
- @trannies = @treasure_map.lookup(Archipelago::Disco::Query.new(@tranny_description), 0)
323
+ update_services!
287
324
  rescue Exception => e
288
325
  puts e
289
326
  pp e.backtrace
@@ -291,6 +328,7 @@ module Archipelago
291
328
  end
292
329
  end
293
330
  end
331
+
294
332
  end
295
333
 
296
334
  end