archipelago 0.1.1 → 0.2.0

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.
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