From nobody at rubyforge.org Mon Nov 13 08:44:30 2006 From: nobody at rubyforge.org (nobody at rubyforge.org) Date: Mon, 13 Nov 2006 08:44:30 -0500 (EST) Subject: [Archipelago-submits] [5] trunk/archipelago/TODO: test commit Message-ID: <20061113134430.DFE54A970007@rubyforge.org> An HTML attachment was scrubbed... URL: http://rubyforge.org/pipermail/archipelago-submits/attachments/20061113/4cfd882c/attachment.html From nobody at rubyforge.org Mon Nov 13 09:04:49 2006 From: nobody at rubyforge.org (nobody at rubyforge.org) Date: Mon, 13 Nov 2006 09:04:49 -0500 (EST) Subject: [Archipelago-submits] [6] trunk/archipelago/TODO: test commit Message-ID: <20061113140449.9FFAEA97000A@rubyforge.org> An HTML attachment was scrubbed... URL: http://rubyforge.org/pipermail/archipelago-submits/attachments/20061113/1eeebb61/attachment.html From nobody at rubyforge.org Mon Nov 13 09:32:53 2006 From: nobody at rubyforge.org (nobody at rubyforge.org) Date: Mon, 13 Nov 2006 09:32:53 -0500 (EST) Subject: [Archipelago-submits] [7] trunk/archipelago/TODO: test commit Message-ID: <20061113143253.23DF1A970007@rubyforge.org> An HTML attachment was scrubbed... URL: http://rubyforge.org/pipermail/archipelago-submits/attachments/20061113/bcff8dcc/attachment.html From nobody at rubyforge.org Mon Nov 13 09:45:26 2006 From: nobody at rubyforge.org (nobody at rubyforge.org) Date: Mon, 13 Nov 2006 09:45:26 -0500 (EST) Subject: [Archipelago-submits] [8] trunk/archipelago/TODO: test commit Message-ID: <20061113144526.EB483A970007@rubyforge.org> An HTML attachment was scrubbed... URL: http://rubyforge.org/pipermail/archipelago-submits/attachments/20061113/3e2ea9e7/attachment.html From nobody at rubyforge.org Mon Nov 13 16:05:01 2006 From: nobody at rubyforge.org (nobody at rubyforge.org) Date: Mon, 13 Nov 2006 16:05:01 -0500 (EST) Subject: [Archipelago-submits] [9] trunk/archipelago/TODO: test commit Message-ID: <20061113210501.5A5BFA97000A@rubyforge.org> Revision: 9 Author: zond Date: 2006-11-13 16:05:00 -0500 (Mon, 13 Nov 2006) Log Message: ----------- test commit Modified Paths: -------------- trunk/archipelago/TODO Modified: trunk/archipelago/TODO =================================================================== --- trunk/archipelago/TODO 2006-11-13 14:45:26 UTC (rev 8) +++ trunk/archipelago/TODO 2006-11-13 21:05:00 UTC (rev 9) @@ -7,4 +7,3 @@ the call. Preferably without incurring performance lossage. * Test the transaction recovery mechanism of Chest. - From nobody at rubyforge.org Mon Nov 13 17:52:23 2006 From: nobody at rubyforge.org (nobody at rubyforge.org) Date: Mon, 13 Nov 2006 17:52:23 -0500 (EST) Subject: [Archipelago-submits] [10] trunk/archipelago/Rakefile: removed old MTN stuff in Rakefile Message-ID: <20061113225223.A9FD452417CA@rubyforge.org> Revision: 10 Author: zond Date: 2006-11-13 17:52:23 -0500 (Mon, 13 Nov 2006) Log Message: ----------- removed old MTN stuff in Rakefile Modified Paths: -------------- trunk/archipelago/Rakefile Modified: trunk/archipelago/Rakefile =================================================================== --- trunk/archipelago/Rakefile 2006-11-13 21:05:00 UTC (rev 9) +++ trunk/archipelago/Rakefile 2006-11-13 22:52:23 UTC (rev 10) @@ -29,7 +29,6 @@ fl.include "#{dir}/**/*" end fl.include "Rakefile" - fl.exclude(/\b_MTN\b/) end Rake::GemPackageTask.new(spec) do |pkg| From nobody at rubyforge.org Mon Nov 13 18:04:27 2006 From: nobody at rubyforge.org (nobody at rubyforge.org) Date: Mon, 13 Nov 2006 18:04:27 -0500 (EST) Subject: [Archipelago-submits] [11] trunk/archipelago: intermediate commit damn you svn Message-ID: <20061113230427.D315652417CA@rubyforge.org> Revision: 11 Author: zond Date: 2006-11-13 18:04:26 -0500 (Mon, 13 Nov 2006) Log Message: ----------- intermediate commit damn you svn Modified Paths: -------------- trunk/archipelago/lib/archipelago.rb trunk/archipelago/lib/disco.rb trunk/archipelago/lib/hashish.rb trunk/archipelago/lib/pirate.rb trunk/archipelago/lib/tranny.rb trunk/archipelago/lib/treasure.rb trunk/archipelago/tests/current_benchmark.rb trunk/archipelago/tests/current_test.rb trunk/archipelago/tests/disco_test.rb trunk/archipelago/tests/pirate_test.rb trunk/archipelago/tests/test_helper.rb trunk/archipelago/tests/tranny_test.rb trunk/archipelago/tests/treasure_benchmark.rb trunk/archipelago/tests/treasure_test.rb Added Paths: ----------- trunk/archipelago/lib/archipelago/ trunk/archipelago/lib/archipelago/current.rb Removed Paths: ------------- trunk/archipelago/lib/current.rb Copied: trunk/archipelago/lib/archipelago/current.rb (from rev 1, trunk/archipelago/lib/current.rb) =================================================================== --- trunk/archipelago/lib/archipelago/current.rb (rev 0) +++ trunk/archipelago/lib/archipelago/current.rb 2006-11-13 23:04:26 UTC (rev 11) @@ -0,0 +1,128 @@ +# Archipelago - a distributed computing toolkit for ruby +# Copyright (C) 2006 Martin Kihlgren +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'monitor' + +# +# Make threads dumpable for show. +# +class Thread + def _dump(l) + "" + end + def self._load(s) + nil + end +end + +module Archipelago + + module Current + + # + # A module that will allow any class to synchronize over any other + # object. + # + module Synchronized + include MonitorMixin + alias :lock :mon_enter + alias :unlock :mon_exit + # + # We dont care about lock ownership. + # + def mon_check_owner + end + # + # Get a lock for this +object+ + # + def lock_on(object) + Thread.exclusive do + this_lock = @archipelago_current_synchronized_lock_by_object[object] ||= Lock.new + this_lock.lock + end + end + # + # Release any lock on this +object+. + # + def unlock_on(object) + Thread.exclusive do + this_lock = @archipelago_current_synchronized_lock_by_object[object] ||= Lock.new + this_lock.unlock + if this_lock.mon_entering_queue.empty? + @archipelago_current_synchronized_lock_by_object.delete(object) + end + end + end + # + # Makes sure the given +block+ is only run once at a time + # for this instance and the given +object+ + # + # Optionally do NOT lock, if you dont +actually+ want to. + # + def synchronize_on(object, actually = true, &block) + lock_on(object) if actually + begin + return yield + ensure + unlock_on(object) if actually + end + end + + private + + # + # Initialize our instance variables + # when someone is extended with us. + # + def self.extend_object(obj) + super(obj) + obj.instance_eval do + sync_initialize + end + end + + # + # Do the actual initialization. + # + def sync_initialize + @archipelago_current_synchronized_lock_by_object = {} + mon_initialize + end + + # + # Initialize upon creation. + # + # Classes that include this module should call super() + # upon initialization. + # + def initialize(*args) + super() + sync_initialize + end + end + + # + # Just a convenience empty class with locking functionality. + # + class Lock + include Synchronized + attr_reader :mon_entering_queue + end + + end + +end Modified: trunk/archipelago/lib/archipelago.rb =================================================================== --- trunk/archipelago/lib/archipelago.rb 2006-11-13 22:52:23 UTC (rev 10) +++ trunk/archipelago/lib/archipelago.rb 2006-11-13 23:04:26 UTC (rev 11) @@ -17,8 +17,8 @@ $: << File.dirname(File.expand_path(__FILE__)) -require 'disco' -require 'current' -require 'tranny' -require 'treasure' -require 'pirate' +require 'archipelago/disco' +require 'archipelago/current' +require 'archipelago/tranny' +require 'archipelago/treasure' +require 'archipelago/pirate' Deleted: trunk/archipelago/lib/current.rb =================================================================== --- trunk/archipelago/lib/current.rb 2006-11-13 22:52:23 UTC (rev 10) +++ trunk/archipelago/lib/current.rb 2006-11-13 23:04:26 UTC (rev 11) @@ -1,128 +0,0 @@ -# Archipelago - a distributed computing toolkit for ruby -# Copyright (C) 2006 Martin Kihlgren -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -require 'monitor' - -# -# Make threads dumpable for show. -# -class Thread - def _dump(l) - "" - end - def self._load(s) - nil - end -end - -module Archipelago - - module Current - - # - # A module that will allow any class to synchronize over any other - # object. - # - module Synchronized - include MonitorMixin - alias :lock :mon_enter - alias :unlock :mon_exit - # - # We dont care about lock ownership. - # - def mon_check_owner - end - # - # Get a lock for this +object+ - # - def lock_on(object) - Thread.exclusive do - this_lock = @archipelago_current_synchronized_lock_by_object[object] ||= Lock.new - this_lock.lock - end - end - # - # Release any lock on this +object+. - # - def unlock_on(object) - Thread.exclusive do - this_lock = @archipelago_current_synchronized_lock_by_object[object] ||= Lock.new - this_lock.unlock - if this_lock.mon_entering_queue.empty? - @archipelago_current_synchronized_lock_by_object.delete(object) - end - end - end - # - # Makes sure the given +block+ is only run once at a time - # for this instance and the given +object+ - # - # Optionally do NOT lock, if you dont +actually+ want to. - # - def synchronize_on(object, actually = true, &block) - lock_on(object) if actually - begin - return yield - ensure - unlock_on(object) if actually - end - end - - private - - # - # Initialize our instance variables - # when someone is extended with us. - # - def self.extend_object(obj) - super(obj) - obj.instance_eval do - sync_initialize - end - end - - # - # Do the actual initialization. - # - def sync_initialize - @archipelago_current_synchronized_lock_by_object = {} - mon_initialize - end - - # - # Initialize upon creation. - # - # Classes that include this module should call super() - # upon initialization. - # - def initialize(*args) - super() - sync_initialize - end - end - - # - # Just a convenience empty class with locking functionality. - # - class Lock - include Synchronized - attr_reader :mon_entering_queue - end - - end - -end Modified: trunk/archipelago/lib/disco.rb =================================================================== --- trunk/archipelago/lib/disco.rb 2006-11-13 22:52:23 UTC (rev 10) +++ trunk/archipelago/lib/disco.rb 2006-11-13 23:04:26 UTC (rev 11) @@ -19,7 +19,7 @@ require 'thread' require 'ipaddr' require 'pp' -require 'current' +require 'archipelago/current' require 'drb' require 'set' Modified: trunk/archipelago/lib/hashish.rb =================================================================== --- trunk/archipelago/lib/hashish.rb 2006-11-13 22:52:23 UTC (rev 10) +++ trunk/archipelago/lib/hashish.rb 2006-11-13 23:04:26 UTC (rev 11) @@ -15,7 +15,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -require 'current' +require 'archipelago/current' require 'bdb' module Archipelago Modified: trunk/archipelago/lib/pirate.rb =================================================================== --- trunk/archipelago/lib/pirate.rb 2006-11-13 22:52:23 UTC (rev 10) +++ trunk/archipelago/lib/pirate.rb 2006-11-13 23:04:26 UTC (rev 11) @@ -15,9 +15,9 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -require 'disco' -require 'tranny' -require 'treasure' +require 'archipelago/disco' +require 'archipelago/tranny' +require 'archipelago/treasure' require 'pp' require 'drb' require 'digest/sha1' Modified: trunk/archipelago/lib/tranny.rb =================================================================== --- trunk/archipelago/lib/tranny.rb 2006-11-13 22:52:23 UTC (rev 10) +++ trunk/archipelago/lib/tranny.rb 2006-11-13 23:04:26 UTC (rev 11) @@ -18,9 +18,10 @@ require 'bdb' require 'pathname' require 'drb' -require 'current' +require 'archipelago/current' require 'digest/sha1' -require 'disco' +require 'archipelago/disco' +require 'archipelago/hashish' module Archipelago Modified: trunk/archipelago/lib/treasure.rb =================================================================== --- trunk/archipelago/lib/treasure.rb 2006-11-13 22:52:23 UTC (rev 10) +++ trunk/archipelago/lib/treasure.rb 2006-11-13 23:04:26 UTC (rev 11) @@ -16,14 +16,14 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require 'drb' -require 'disco' +require 'archipelago/disco' require 'bdb' require 'pathname' require 'digest/sha1' require 'pp' require 'set' -require 'hashish' -require 'tranny' +require 'archipelago/hashish' +require 'archipelago/tranny' module Archipelago Modified: trunk/archipelago/tests/current_benchmark.rb =================================================================== --- trunk/archipelago/tests/current_benchmark.rb 2006-11-13 22:52:23 UTC (rev 10) +++ trunk/archipelago/tests/current_benchmark.rb 2006-11-13 23:04:26 UTC (rev 11) @@ -1,6 +1,5 @@ require File.join(File.dirname(__FILE__), 'test_helper') -require 'current' class CurrentBenchmark < Test::Unit::TestCase Modified: trunk/archipelago/tests/current_test.rb =================================================================== --- trunk/archipelago/tests/current_test.rb 2006-11-13 22:52:23 UTC (rev 10) +++ trunk/archipelago/tests/current_test.rb 2006-11-13 23:04:26 UTC (rev 11) @@ -1,6 +1,5 @@ require File.join(File.dirname(__FILE__), 'test_helper') -require 'current' class CurrentTest < Test::Unit::TestCase Modified: trunk/archipelago/tests/disco_test.rb =================================================================== --- trunk/archipelago/tests/disco_test.rb 2006-11-13 22:52:23 UTC (rev 10) +++ trunk/archipelago/tests/disco_test.rb 2006-11-13 23:04:26 UTC (rev 11) @@ -1,10 +1,5 @@ require File.join(File.dirname(__FILE__), 'test_helper') -require 'disco' -require 'drb' -require 'socket' -require 'ipaddr' -require 'thread' class RemoteValidator include DRb::DRbUndumped Modified: trunk/archipelago/tests/pirate_test.rb =================================================================== --- trunk/archipelago/tests/pirate_test.rb 2006-11-13 22:52:23 UTC (rev 10) +++ trunk/archipelago/tests/pirate_test.rb 2006-11-13 23:04:26 UTC (rev 11) @@ -1,10 +1,5 @@ require File.join(File.dirname(__FILE__), 'test_helper') -require 'treasure' -require 'drb' -require 'tranny' -require 'hashish' -require 'pirate' class PirateTest < Test::Unit::TestCase Modified: trunk/archipelago/tests/test_helper.rb =================================================================== --- trunk/archipelago/tests/test_helper.rb 2006-11-13 22:52:23 UTC (rev 10) +++ trunk/archipelago/tests/test_helper.rb 2006-11-13 23:04:26 UTC (rev 11) @@ -3,10 +3,13 @@ $: << File.join(home, "..", "lib") require 'pp' +require 'drb' require 'test/unit' -require 'tranny' -require 'treasure' +require 'archipelago' require 'benchmark' +require 'socket' +require 'ipaddr' +require 'thread' class TestTransaction def join(o) Modified: trunk/archipelago/tests/tranny_test.rb =================================================================== --- trunk/archipelago/tests/tranny_test.rb 2006-11-13 22:52:23 UTC (rev 10) +++ trunk/archipelago/tests/tranny_test.rb 2006-11-13 23:04:26 UTC (rev 11) @@ -1,7 +1,5 @@ require File.join(File.dirname(__FILE__), 'test_helper') -require 'tranny' -require 'drb' class TrannyTest < Test::Unit::TestCase Modified: trunk/archipelago/tests/treasure_benchmark.rb =================================================================== --- trunk/archipelago/tests/treasure_benchmark.rb 2006-11-13 22:52:23 UTC (rev 10) +++ trunk/archipelago/tests/treasure_benchmark.rb 2006-11-13 23:04:26 UTC (rev 11) @@ -1,8 +1,5 @@ require File.join(File.dirname(__FILE__), 'test_helper') -require 'treasure' -require 'drb' -require 'hashish' class TreasureBenchmark < Test::Unit::TestCase Modified: trunk/archipelago/tests/treasure_test.rb =================================================================== --- trunk/archipelago/tests/treasure_test.rb 2006-11-13 22:52:23 UTC (rev 10) +++ trunk/archipelago/tests/treasure_test.rb 2006-11-13 23:04:26 UTC (rev 11) @@ -1,9 +1,5 @@ require File.join(File.dirname(__FILE__), 'test_helper') -require 'treasure' -require 'drb' -require 'tranny' -require 'hashish' class TreasureTest < Test::Unit::TestCase From nobody at rubyforge.org Mon Nov 13 19:27:05 2006 From: nobody at rubyforge.org (nobody at rubyforge.org) Date: Mon, 13 Nov 2006 19:27:05 -0500 (EST) Subject: [Archipelago-submits] [13] trunk/archipelago: added :published_at as required field in Records. Message-ID: <20061114002705.71D8BA970014@rubyforge.org> Revision: 13 Author: zond Date: 2006-11-13 19:27:04 -0500 (Mon, 13 Nov 2006) Log Message: ----------- added :published_at as required field in Records. added evaluate! method to Chest to evaluate new classes remotely. made pirate send evaluate! calls to new chests. Modified Paths: -------------- trunk/archipelago/lib/archipelago/disco.rb trunk/archipelago/lib/archipelago/pirate.rb trunk/archipelago/lib/archipelago/treasure.rb trunk/archipelago/scripts/chest.rb trunk/archipelago/scripts/pirate.rb trunk/archipelago/scripts/tranny.rb trunk/archipelago/tests/disco_test.rb trunk/archipelago/tests/treasure_test.rb Modified: trunk/archipelago/lib/archipelago/disco.rb =================================================================== --- trunk/archipelago/lib/archipelago/disco.rb 2006-11-13 23:06:06 UTC (rev 12) +++ trunk/archipelago/lib/archipelago/disco.rb 2006-11-14 00:27:04 UTC (rev 13) @@ -107,6 +107,7 @@ # def publish!(options = {}) @jockey ||= Archipelago::Disco::Jockey.new(@jockey_options.merge(options[:jockey_options] || {})) + @service_description.merge!({:published_at => Time.now}) @jockey.publish(Archipelago::Disco::Record.new(@service_description.merge(options[:service_description] || {}))) end @@ -203,8 +204,9 @@ # Initialize this Record with a hash that must contain an :service_id and a :validator. # def initialize(hash) - raise "Record must have an :service_id" unless hash.include?(:service_id) + raise "Record must have a :service_id" unless hash.include?(:service_id) raise "Record must have a :validator" unless hash.include?(:validator) + raise "Record must have a :published_at" unless hash.include?(:published_at) super(hash) end # Modified: trunk/archipelago/lib/archipelago/pirate.rb =================================================================== --- trunk/archipelago/lib/archipelago/pirate.rb 2006-11-13 23:06:06 UTC (rev 12) +++ trunk/archipelago/lib/archipelago/pirate.rb 2006-11-14 00:27:04 UTC (rev 13) @@ -74,6 +74,9 @@ # Will look for Archipelago::Treasure::Chests matching :chest_description or CHEST_DESCRIPTION and # Archipelago::Tranny::Managers matching :tranny_description or TRANNY_DESCRIPTION. # + # Will send off all :chest_eval_files to any chest found for possible evaluation to ensure existence + # of required classes and modules at the chest. + # def initialize(options = {}) @treasure_map = Archipelago::Disco::Jockey.new(options[:jockey_options] || {}) @@ -81,6 +84,10 @@ @tranny_description = TRANNY_DESCRIPTION.merge(options[:tranny_description] || {}) @jockey_options = options[:jockey_options] || {} + @chest_eval_files = options[:chest_eval_files] || [] + + @chests_having_evaluated = Set.new + start_service_updater(options[:initial_service_update_interval] || INITIAL_SERVICE_UPDATE_INTERVAL, options[:maximum_service_update_interval] || MAXIMUM_SERVICE_UPDATE_INTERVAL) @@ -200,11 +207,32 @@ end # + # Get all chests from the Archipelago::Disco::Jockey and + # send our @chest_eval_files to it if we have not already. + # + def get_chests + @chests = @treasure_map.lookup(Archipelago::Disco::Query.new(@chest_description), 0) + @chests.values.each do |chest| + unless @chests_having_evaluated.include?([chest[:service_id], chest[:published_at]]) + @chest_eval_files.each do |filename| + begin + chest[:service].evaluate!(filename, open(filename).read) + rescue Exception => e + puts e + pp e.backtrace + end + end + @chests_having_evaluated << [chest[:service_id], chest[:published_at]] + end + end + end + + # # Start a thread looking up existing chests between every # +initial+ and +maximum+ seconds. # def start_service_updater(initial, maximum) - @chests = @treasure_map.lookup(Archipelago::Disco::Query.new(@chest_description), 0) + get_chests @trannies = @treasure_map.lookup(Archipelago::Disco::Query.new(@tranny_description), 0) Thread.start do standoff = initial @@ -213,7 +241,7 @@ sleep(standoff) standoff *= 2 standoff = maximum if standoff > maximum - @chests = @treasure_map.lookup(Archipelago::Disco::Query.new(@chest_description), 0) + get_chests @trannies = @treasure_map.lookup(Archipelago::Disco::Query.new(@tranny_description), 0) rescue Exception => e puts e Modified: trunk/archipelago/lib/archipelago/treasure.rb =================================================================== --- trunk/archipelago/lib/archipelago/treasure.rb 2006-11-13 23:06:06 UTC (rev 12) +++ trunk/archipelago/lib/archipelago/treasure.rb 2006-11-14 00:27:04 UTC (rev 13) @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require 'drb' -require 'archipelago/disco' require 'bdb' require 'pathname' require 'digest/sha1' @@ -24,6 +23,7 @@ require 'set' require 'archipelago/hashish' require 'archipelago/tranny' +require 'archipelago/disco' module Archipelago @@ -214,6 +214,13 @@ @crashed = Set.new # + # [label,label...] + # To know what data we have already evaluated + # so that we dont overdo it. + # + @seen_data = Set.new + + # # The magical persistent map that defines how we actually # store our data. # @@ -231,6 +238,22 @@ end # + # Evaluate +data+ if we have not already seen +label+. + # + def evaluate!(label, data) + unless @seen_data.include?(label) + begin + Object.class_eval(data) + rescue Exception => e + puts e + pp e.backtrace + ensure + @seen_data << label + end + end + end + + # # Return the contents of this chest using a given +key+ and +transaction+. # def [](key, transaction = nil) Modified: trunk/archipelago/scripts/chest.rb =================================================================== --- trunk/archipelago/scripts/chest.rb 2006-11-13 23:06:06 UTC (rev 12) +++ trunk/archipelago/scripts/chest.rb 2006-11-14 00:27:04 UTC (rev 13) @@ -7,7 +7,7 @@ $: << File.join(File.dirname(__FILE__), "..", "lib") -require 'treasure' +require 'archipelago/treasure' if ARGV.size > 1 DRb.start_service(ARGV[1]) Modified: trunk/archipelago/scripts/pirate.rb =================================================================== --- trunk/archipelago/scripts/pirate.rb 2006-11-13 23:06:06 UTC (rev 12) +++ trunk/archipelago/scripts/pirate.rb 2006-11-14 00:27:04 UTC (rev 13) @@ -1,6 +1,6 @@ #!/usr/bin/env ruby -require 'pirate' +require 'archipelago/pirate' DRb.start_service("druby://localhost:#{rand(1000) + 5000}") @p = Archipelago::Pirate::Captain.new Modified: trunk/archipelago/scripts/tranny.rb =================================================================== --- trunk/archipelago/scripts/tranny.rb 2006-11-13 23:06:06 UTC (rev 12) +++ trunk/archipelago/scripts/tranny.rb 2006-11-14 00:27:04 UTC (rev 13) @@ -7,7 +7,7 @@ $: << File.join(File.dirname(__FILE__), "..", "lib") -require 'treasure' +require 'archipelago/treasure' if ARGV.size > 1 DRb.start_service(ARGV[1]) Modified: trunk/archipelago/tests/disco_test.rb =================================================================== --- trunk/archipelago/tests/disco_test.rb 2006-11-13 23:06:06 UTC (rev 12) +++ trunk/archipelago/tests/disco_test.rb 2006-11-14 00:27:04 UTC (rev 13) @@ -22,8 +22,9 @@ @d2 = TestJockey.new(:thrifty_publishing => false) @v1 = RemoteValidator.new(true) @p1 = Archipelago::Disco::Record.new(:service_id => 1, - :validator => DRbObject.new(@v1), - :epa => "blar") + :published_at => Time.now, + :validator => DRbObject.new(@v1), + :epa => "blar") @d1.publish(@p1) assert(!@d2.lookup(Archipelago::Disco::Query.new(:epa => "blar")).empty?) @@ -73,8 +74,9 @@ assert(empty) @d1.publish(Archipelago::Disco::Record.new(:service_id => 1, - :validator => Archipelago::Disco::MockValidator.new, - :epa => "blar2")) + :published_at => Time.now, + :validator => Archipelago::Disco::MockValidator.new, + :epa => "blar2")) assert_within(0.5) do !empty @@ -89,7 +91,10 @@ def test_thrifty_publishing @ltq.clear - @d1.publish(Archipelago::Disco::Record.new(:glada => "jaa", :validator => Archipelago::Disco::MockValidator.new, :service_id => 344)) + @d1.publish(Archipelago::Disco::Record.new(:glada => "jaa", + :published_at => Time.now, + :validator => Archipelago::Disco::MockValidator.new, + :service_id => 344)) sleep 0.1 assert(@ltq.any? do |d| o = Marshal.load(d) @@ -106,7 +111,10 @@ @ltq.clear c3 = Archipelago::Disco::Jockey.new(:thrifty_publishing => true) - c3.publish(Archipelago::Disco::Record.new(:glad => "ja", :validator => Archipelago::Disco::MockValidator.new, :service_id => 33)) + c3.publish(Archipelago::Disco::Record.new(:glad => "ja", + :published_at => Time.now, + :validator => Archipelago::Disco::MockValidator.new, + :service_id => 33)) sleep 0.1 assert(@ltq.empty?) @@ -118,7 +126,10 @@ end def test_thrifty_replying - @d1.publish(Archipelago::Disco::Record.new(:gladaa => "jaaa", :validator => Archipelago::Disco::MockValidator.new, :service_id => 3444)) + @d1.publish(Archipelago::Disco::Record.new(:gladaa => "jaaa", + :published_at => Time.now, + :validator => Archipelago::Disco::MockValidator.new, + :service_id => 3444)) @ltq.clear assert(!@d2.lookup(Archipelago::Disco::Query.new(:gladaa => "jaaa")).empty?) @@ -136,7 +147,10 @@ @d1.stop @d2.stop c3 = Archipelago::Disco::Jockey.new(:thrifty_replying => true, :thrifty_publishing => true) - c3.publish(Archipelago::Disco::Record.new(:glad2 => "ja2", :validator => Archipelago::Disco::MockValidator.new, :service_id => 34)) + c3.publish(Archipelago::Disco::Record.new(:glad2 => "ja2", + :published_at => Time.now, + :validator => Archipelago::Disco::MockValidator.new, + :service_id => 34)) @ltq.clear @@ -157,14 +171,20 @@ end def test_thrifty_caching - @d2.publish(Archipelago::Disco::Record.new(:bojkotta => "jag", :validator => Archipelago::Disco::MockValidator.new, :service_id => 411)) + @d2.publish(Archipelago::Disco::Record.new(:bojkotta => "jag", + :published_at => Time.now, + :validator => Archipelago::Disco::MockValidator.new, + :service_id => 411)) sleep 0.1 assert(@d1.remote_services.include?(411)) c1 = TestJockey.new(:thrifty_caching => true) assert(!c1.local_services.include?(41)) assert(!c1.remote_services.include?(41)) - @d1.publish(Archipelago::Disco::Record.new(:bojkott => "ja", :validator => Archipelago::Disco::MockValidator.new, :service_id => 41)) + @d1.publish(Archipelago::Disco::Record.new(:bojkott => "ja", + :published_at => Time.now, + :validator => Archipelago::Disco::MockValidator.new, + :service_id => 41)) sleep 0.1 assert(!c1.local_services.include?(41)) assert(!c1.remote_services.include?(41)) Modified: trunk/archipelago/tests/treasure_test.rb =================================================================== --- trunk/archipelago/tests/treasure_test.rb 2006-11-13 23:06:06 UTC (rev 12) +++ trunk/archipelago/tests/treasure_test.rb 2006-11-14 00:27:04 UTC (rev 13) @@ -17,6 +17,15 @@ DRb.stop_service end + def test_eval + @c.evaluate!("burk", "class Burk; end") + b = Burk.new + @c.evaluate!("burk", "class Bong; end") + assert_raise(NameError) do + b = Bong.new + end + end + def test_store_load_update s = "hehu" @c["oj"] = "hehu" From nobody at rubyforge.org Mon Nov 13 19:34:37 2006 From: nobody at rubyforge.org (nobody at rubyforge.org) Date: Mon, 13 Nov 2006 19:34:37 -0500 (EST) Subject: [Archipelago-submits] [14] trunk/archipelago: added a test for the automatic Chest#evaluate! call in Pirate::Captain Message-ID: <20061114003437.6298B5241951@rubyforge.org> Revision: 14 Author: zond Date: 2006-11-13 19:34:37 -0500 (Mon, 13 Nov 2006) Log Message: ----------- added a test for the automatic Chest#evaluate! call in Pirate::Captain Modified Paths: -------------- trunk/archipelago/lib/archipelago/disco.rb trunk/archipelago/lib/archipelago/pirate.rb trunk/archipelago/tests/disco_test.rb trunk/archipelago/tests/pirate_test.rb Added Paths: ----------- trunk/archipelago/tests/evaltest Modified: trunk/archipelago/lib/archipelago/disco.rb =================================================================== --- trunk/archipelago/lib/archipelago/disco.rb 2006-11-14 00:27:04 UTC (rev 13) +++ trunk/archipelago/lib/archipelago/disco.rb 2006-11-14 00:34:37 UTC (rev 14) @@ -368,7 +368,7 @@ # # Stops all the threads in this instance. # - def stop + def stop! @listener_thread.kill @unilistener_thread.kill @shouter_thread.kill Modified: trunk/archipelago/lib/archipelago/pirate.rb =================================================================== --- trunk/archipelago/lib/archipelago/pirate.rb 2006-11-14 00:27:04 UTC (rev 13) +++ trunk/archipelago/lib/archipelago/pirate.rb 2006-11-14 00:34:37 UTC (rev 14) @@ -65,7 +65,7 @@ # Archipelago::Treasure:Dubloons in them. # class Captain - attr_reader :chests, :treasure_map, :trannies, :transaction + attr_reader :chests, :treasure_map, :trannies # # Initialize an instance using an Archipelago::Disco::Jockey with :jockey_options # if given, that looks for new services :initial_service_update_interval or INITIAL_SERVICE_UPDATE_INTERVAL, @@ -190,6 +190,13 @@ 'yar!' end + # + # Stops the service update thread for this Pirate. + # + def stop! + @service_update_thread.kill + end + private # @@ -234,7 +241,7 @@ def start_service_updater(initial, maximum) get_chests @trannies = @treasure_map.lookup(Archipelago::Disco::Query.new(@tranny_description), 0) - Thread.start do + @service_update_thread = Thread.start do standoff = initial loop do begin Modified: trunk/archipelago/tests/disco_test.rb =================================================================== --- trunk/archipelago/tests/disco_test.rb 2006-11-14 00:27:04 UTC (rev 13) +++ trunk/archipelago/tests/disco_test.rb 2006-11-14 00:34:37 UTC (rev 14) @@ -58,8 +58,8 @@ end def teardown - @d1.stop - @d2.stop + @d1.stop! + @d2.stop! DRb.stop_service @lt.kill @listener.close @@ -106,8 +106,8 @@ end end) - @d1.stop - @d2.stop + @d1.stop! + @d2.stop! @ltq.clear c3 = Archipelago::Disco::Jockey.new(:thrifty_publishing => true) @@ -121,8 +121,8 @@ c1 = Archipelago::Disco::Jockey.new(:thrifty_publishing => true) assert(!c1.lookup(Archipelago::Disco::Query.new(:glad => "ja")).empty?) - c1.stop - c3.stop + c1.stop! + c3.stop! end def test_thrifty_replying @@ -144,8 +144,8 @@ end) - @d1.stop - @d2.stop + @d1.stop! + @d2.stop! c3 = Archipelago::Disco::Jockey.new(:thrifty_replying => true, :thrifty_publishing => true) c3.publish(Archipelago::Disco::Record.new(:glad2 => "ja2", :published_at => Time.now, @@ -166,8 +166,8 @@ end end) - c1.stop - c3.stop + c1.stop! + c3.stop! end def test_thrifty_caching Added: trunk/archipelago/tests/evaltest =================================================================== --- trunk/archipelago/tests/evaltest (rev 0) +++ trunk/archipelago/tests/evaltest 2006-11-14 00:34:37 UTC (rev 14) @@ -0,0 +1,3 @@ +class Evaltest + +end Modified: trunk/archipelago/tests/pirate_test.rb =================================================================== --- trunk/archipelago/tests/pirate_test.rb 2006-11-14 00:27:04 UTC (rev 13) +++ trunk/archipelago/tests/pirate_test.rb 2006-11-14 00:34:37 UTC (rev 14) @@ -22,12 +22,30 @@ end def teardown + @p.stop! @c.persistence_provider.unlink @c2.persistence_provider.unlink @tm.persistence_provider.unlink DRb.stop_service end + def test_evaluate + assert_raise(NameError) do + e = Evaltest.new + end + + p2 = Archipelago::Pirate::Captain.new(:chest_description => {:class => "TestChest"}, + :tranny_description => {:class => "TestManager"}, + :chest_eval_files => File.join(File.dirname(__FILE__), 'evaltest')) + assert_within(10) do + !p2.chests.empty? + end + + e = Evaltest.new + + p2.stop! + end + def test_write_read 0.upto(100) do |n| @p["#{n}"] = "#{n}" From nobody at rubyforge.org Mon Nov 13 18:06:07 2006 From: nobody at rubyforge.org (nobody at rubyforge.org) Date: Mon, 13 Nov 2006 18:06:07 -0500 (EST) Subject: [Archipelago-submits] [12] trunk/archipelago/lib: moved all chunky source files into an archipelago subdir to reduce the require-namespace-problems Message-ID: <20061113230607.C5B7052417CA@rubyforge.org> Revision: 12 Author: zond Date: 2006-11-13 18:06:06 -0500 (Mon, 13 Nov 2006) Log Message: ----------- moved all chunky source files into an archipelago subdir to reduce the require-namespace-problems Added Paths: ----------- trunk/archipelago/lib/archipelago/disco.rb trunk/archipelago/lib/archipelago/hashish.rb trunk/archipelago/lib/archipelago/pirate.rb trunk/archipelago/lib/archipelago/tranny.rb trunk/archipelago/lib/archipelago/treasure.rb Removed Paths: ------------- trunk/archipelago/lib/disco.rb trunk/archipelago/lib/hashish.rb trunk/archipelago/lib/pirate.rb trunk/archipelago/lib/tranny.rb trunk/archipelago/lib/treasure.rb Copied: trunk/archipelago/lib/archipelago/disco.rb (from rev 11, trunk/archipelago/lib/disco.rb) =================================================================== --- trunk/archipelago/lib/archipelago/disco.rb (rev 0) +++ trunk/archipelago/lib/archipelago/disco.rb 2006-11-13 23:06:06 UTC (rev 12) @@ -0,0 +1,548 @@ +# Archipelago - a distributed computing toolkit for ruby +# Copyright (C) 2006 Martin Kihlgren +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'socket' +require 'thread' +require 'ipaddr' +require 'pp' +require 'archipelago/current' +require 'drb' +require 'set' + +module Archipelago + + module Disco + + # + # Default address to use. + # + ADDRESS = "234.2.4.2" + # + # Default port to use. + # + PORT = 25242 + # + # Default port range to use for unicast. + # + UNIPORTS = 25243..26243 + # + # Default lookup timeout. + # + LOOKUP_TIMEOUT = 10 + # + # Default initial pause between resending lookup queries. + # Will be doubled for each resend. + # + INITIAL_LOOKUP_STANDOFF = 0.1 + # + # Default pause between trying to validate all services we + # know about. + # + VALIDATION_INTERVAL = 60 + # + # Only save stuff that we KNOW we want. + # + THRIFTY_CACHING = true + # + # Only reply to the one actually asking about a service. + # + THRIFTY_REPLYING = true + # + # Dont send on publish, only on query. + # + THRIFTY_PUBLISHING = false + + # + # A module to simplify publishing services. + # + # If you include it you can use the publish! method + # at your convenience. + # + # If you want to customize the publishing related behaviour you can + # call initialize_publishable with a Hash of options. + # + # See Archipelago::Treasure::Chest or Archipelago::Tranny::Manager for examples. + # + # It will store the service_id of this service in a directory beside this + # file (publishable.rb) named as the class you include into unless you + # define @persistence_provider before you call initialize_publishable. + # + module Publishable + + # + # Will initialize this instance with @service_description and @jockey_options + # and merge these with the optionally given :service_description and + # :jockey_options. + # + def initialize_publishable(options = {}) + @service_description = { + :service_id => service_id, + :validator => DRbObject.new(self), + :service => DRbObject.new(self), + :class => self.class.name + }.merge(options[:service_description] || {}) + @jockey_options = options[:jockey_options] || {} + end + + # + # Create an Archipelago::Disco::Jockey for this instance using @jockey_options + # or optionally given :jockey_options. + # + # Will publish this service using @service_description or optionally given + # :service_description. + # + def publish!(options = {}) + @jockey ||= Archipelago::Disco::Jockey.new(@jockey_options.merge(options[:jockey_options] || {})) + @jockey.publish(Archipelago::Disco::Record.new(@service_description.merge(options[:service_description] || {}))) + end + + # + # We are always valid if we are able to reply. + # + def valid? + true + end + + # + # Returns our semi-unique id so that we can be found again. + # + def service_id + # + # The provider of happy magic persistent hashes of different kinds. + # + @persistence_provider ||= Archipelago::Hashish::BerkeleyHashishProvider.new(Pathname.new(File.expand_path(__FILE__)).parent.join(self.class.name + ".db")) + # + # Stuff that didnt fit in any of the other databases. + # + @metadata ||= @persistence_provider.get_hashish("metadata") + service_id = @metadata["service_id"] + unless service_id + host = "#{Socket::gethostbyname(Socket::gethostname)[0]}" rescue "localhost" + service_id = @metadata["service_id"] ||= Digest::SHA1.hexdigest("#{host}:#{Time.new.to_f}:#{self.object_id}:#{rand(1 << 32)}") + end + return service_id + end + + end + + # + # A mock validator to be used for dumb systems that dont want + # to validate. + # + class MockValidator + def valid? + true + end + end + + # + # A Hash-like description of a service. + # + class ServiceDescription + IGNORABLE_ATTRIBUTES = Set[:unicast_reply] + attr_reader :attributes + # + # Initialize this service description with a hash + # that describes its attributes. + # + def initialize(hash = {}) + @attributes = hash + end + # + # Forwards as much as possible to our Hash. + # + def method_missing(meth, *args, &block) + if @attributes.respond_to?(meth) + if block + @attributes.send(meth, *args, &block) + else + @attributes.send(meth, *args) + end + else + super(*args) + end + end + # + # Returns whether this ServiceDescription matches the given +match+. + # + def matches?(match) + match.each do |key, value| + unless IGNORABLE_ATTRIBUTES.include?(key) + return false unless @attributes.include?(key) && (value.nil? || @attributes[key] == value) + end + end + true + end + end + + # + # A class used to query the Disco network for services. + # + class Query < ServiceDescription + end + + # + # A class used to define an existing service. + # + class Record < ServiceDescription + # + # Initialize this Record with a hash that must contain an :service_id and a :validator. + # + def initialize(hash) + raise "Record must have an :service_id" unless hash.include?(:service_id) + raise "Record must have a :validator" unless hash.include?(:validator) + super(hash) + end + # + # Returns whether this service is still valid. + # + def valid? + begin + self[:validator].valid? + rescue DRb::DRbError => e + false + end + end + end + + # + # A container of services. + # + class ServiceLocker + attr_reader :hash + include Archipelago::Current::Synchronized + def initialize(hash = nil) + super + @hash = hash || {} + end + # + # Merge this locker with another. + # + def merge(sd) + rval = @hash.clone + rval.merge!(sd.hash) + ServiceLocker.new(rval) + end + # + # Forwards as much as possible to our Hash. + # + def method_missing(meth, *args, &block) + if @hash.respond_to?(meth) + synchronize do + if block + @hash.send(meth, *args, &block) + else + @hash.send(meth, *args) + end + end + else + super(*args) + end + end + # + # Find all containing services matching +match+. + # + def get_services(match) + rval = ServiceLocker.new + self.each do |service_id, service_data| + rval[service_id] = service_data if service_data.matches?(match) && service_data.valid? + end + return rval + end + # + # Remove all non-valid services. + # + def validate! + self.clone.each do |service_id, service_data| + self.delete(service_id) unless service_data.valid? + end + end + end + + # + # The main discovery class used to both publish and lookup services. + # + class Jockey + + attr_reader :new_service_semaphore + + # + # Will create a Jockey service running on :address and :port or + # ADDRESS and PORT if none are given. + # + # Will the first available unicast port within :uniports or if not given UNIPORTS for receiving unicast messages. + # + # Will have a default :lookup_timeout of LOOKUP_TIMEOUT, a default + # :initial_lookup_standoff of INITIAL_LOOKUP_STANDOFF and a default + # :validation_interval of VALIDATION_INTERVAL. + # + # Will only cache (and validate, which saves network traffic) stuff + # that has been looked up before if :thrifty_caching, or THRIFTY_CACHING if not given. + # + # Will only reply to the one that sent out the query (and therefore save lots of network traffic) + # if :thrifty_replying, or THRIFTY_REPLYING if not given. + # + # Will send out a multicast when a new service is published unless :thrifty_publishing, or + # THRIFTY_PUBLISHING if not given. + # + # Will reply to all queries to which it has matching local services with a unicast message if :thrifty_replying, + # or if not given THRIFTY_REPLYING. Otherwise will reply with multicasts. + # + def initialize(options = {}) + @thrifty_caching = options.include?(:thrifty_caching) ? options[:thrifty_caching] : THRIFTY_CACHING + @thrifty_replying = options.include?(:thrifty_replying) ? options[:thrifty_replying] : THRIFTY_REPLYING + @thrifty_publishing = options.include?(:thrifty_publishing) ? options[:thrifty_publishing] : THRIFTY_PUBLISHING + @lookup_timeout = options[:lookup_timeout] || LOOKUP_TIMEOUT + @initial_lookup_standoff = options[:initial_lookup_standoff] || INITIAL_LOOKUP_STANDOFF + + @remote_services = ServiceLocker.new + @local_services = ServiceLocker.new + @subscribed_services = Set.new + + @incoming = Queue.new + @outgoing = Queue.new + + @new_service_semaphore = MonitorMixin::ConditionVariable.new(Archipelago::Current::Lock.new) + + @listener = UDPSocket.new + @unilistener = UDPSocket.new + + @listener.setsockopt(Socket::IPPROTO_IP, + Socket::IP_ADD_MEMBERSHIP, + IPAddr.new(options[:address] || ADDRESS).hton + Socket.gethostbyname("0.0.0.0")[3]) + + @listener.setsockopt(Socket::SOL_SOCKET, + Socket::SO_REUSEADDR, + true) + begin + @listener.setsockopt(Socket::SOL_SOCKET, + Socket::SO_REUSEPORT, + true) + rescue + # /moo + end + @listener.bind('', options[:port] || PORT) + + uniports = options[:uniports] || UNIPORTS + this_port = uniports.min + begin + @unilistener.bind('', this_port) + rescue Errno::EADDRINUSE => e + if this_port < uniports.max + this_port += 1 + retry + else + raise e + end + end + @unicast_address = "#{Socket::gethostbyname(Socket::gethostname)[0]}:#{this_port}" rescue "localhost:#{this_port}" + + @sender = UDPSocket.new + @sender.connect(options[:address] || ADDRESS, options[:port] || PORT) + + @unisender = UDPSocket.new + + start_listener + start_unilistener + start_shouter + start_picker + start_validator(options[:validation_interval] || VALIDATION_INTERVAL) + end + + # + # Stops all the threads in this instance. + # + def stop + @listener_thread.kill + @unilistener_thread.kill + @shouter_thread.kill + @picker_thread.kill + @validator_thread.kill + end + + # + # Lookup any services matching +match+, optionally with a +timeout+. + # + # Will immediately return if we know of matching and valid services, + # will otherwise send out regular Queries and return as soon as + # matching services are found, or when the +timeout+ runs out. + # + def lookup(match, timeout = @lookup_timeout) + match[:unicast_reply] = @unicast_address + @subscribed_services << match if @thrifty_caching + standoff = @initial_lookup_standoff + + @outgoing << [nil, match] + known_services = @remote_services.get_services(match).merge(@local_services.get_services(match)) + return known_services unless known_services.empty? + + @new_service_semaphore.wait(standoff) + standoff *= 2 + + t = Time.new + while Time.new < t + timeout + known_services = @remote_services.get_services(match).merge(@local_services.get_services(match)) + return known_services unless known_services.empty? + + @new_service_semaphore.wait(standoff) + standoff *= 2 + + @outgoing << [nil, match] + end + + ServiceLocker.new + end + + # + # Record the given +service+ and broadcast about it. + # + def publish(service) + if service.valid? + @local_services[service[:service_id]] = service + @new_service_semaphore.broadcast + unless @thrifty_publishing + @outgoing << [nil, service] + end + end + end + + private + + # + # Start the validating thread. + # + def start_validator(validation_interval) + @validator_thread = Thread.new do + loop do + begin + @local_services.validate! + @remote_services.validate! + sleep(validation_interval) + rescue Exception => e + puts e + pp e.backtrace + end + end + end + end + + # + # Start the thread sending Records and Queries + # + def start_shouter + @shouter_thread = Thread.new do + loop do + begin + recipient, data = @outgoing.pop + if recipient + address, port = recipient.split(/:/) + @unisender.send(Marshal.dump(data), 0, address, port.to_i) + else + begin + @sender.write(Marshal.dump(data)) + rescue Errno::ECONNREFUSED => e + retry + end + end + rescue Exception => e + puts e + pp e.backtrace + end + end + end + end + + # + # Start the thread receiving Records and Queries + # + def start_listener + @listener_thread = Thread.new do + loop do + begin + @incoming << Marshal.load(@listener.recv(1024)) + rescue Exception => e + puts e + pp e.backtrace + end + end + end + end + + # + # Start the thread receiving Records and Queries + # on unicast. + # + def start_unilistener + @unilistener_thread = Thread.new do + loop do + begin + @incoming << Marshal.load(@unilistener.recv(1024)) + rescue Exception => e + puts e + pp e.backtrace + end + end + end + end + + # + # Start the thread picking incoming Records and Queries and + # handling them properly + # + def start_picker + @picker_thread = Thread.new do + loop do + begin + data = @incoming.pop + if Archipelago::Disco::Query === data + @local_services.get_services(data).each do |service_id, service_data| + if @thrifty_replying + @outgoing << [data[:unicast_reply], service_data] + else + @outgoing << [nil, service_data] + end + end + elsif Archipelago::Disco::Record === data + if interesting?(data) && data.valid? + @remote_services[data[:service_id]] = data + @new_service_semaphore.broadcast + end + end + rescue Exception => e + puts e + pp e.backtrace + end + end + end + end + + # + # Are we generous in our caching, or have we been + # asked about this type of +publish+ before? + # + def interesting?(publish) + @subscribed_services.each do |subscribed| + return true if publish.matches?(subscribed) + end + return !@thrifty_caching + end + + end + + end + +end Copied: trunk/archipelago/lib/archipelago/hashish.rb (from rev 11, trunk/archipelago/lib/hashish.rb) =================================================================== --- trunk/archipelago/lib/archipelago/hashish.rb (rev 0) +++ trunk/archipelago/lib/archipelago/hashish.rb 2006-11-13 23:06:06 UTC (rev 12) @@ -0,0 +1,199 @@ +# Archipelago - a distributed computing toolkit for ruby +# Copyright (C) 2006 Martin Kihlgren +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'archipelago/current' +require 'bdb' + +module Archipelago + + # + # The module containing the persistence default provider. + # + module Hashish + + # + # In essence a Berkeley Database backed Hash. + # + # Will cache all values having been written or read + # in a normal Hash cache for fast access. + # + # Will save the last update timestamp for all keys + # in a separate Hash cache AND a separate Berkeley Database. + # + class BerkeleyHashish + include Archipelago::Current::Synchronized + # + # Initialize an instance with the +name+ and BDB::Env +env+. + # + def initialize(name, env) + super() + @content_db = env.open_db(BDB::HASH, name, "content", BDB::CREATE) + @content = {} + @timestamps_db = env.open_db(BDB::HASH, name, "timestamps", BDB::CREATE | BDB::NOMMAP) + @timestamps = {} + @lock = Archipelago::Current::Lock.new + end + # + # Returns a deep ( Marshal.load(Marshal.dump(o)) ) clone + # of the object represented by +key+. + # + def get_deep_clone(key) + return Marshal.load(@content_db[Marshal.dump(key)]) + end + # + # Simply get the value for the +key+. + # + def [](key) + @lock.synchronize_on(key) do + + value = @content[key] + return value if value + + return get_from_db(key) + + end + end + # + # Insert +value+ under +key+. + # + def []=(key, value) + @lock.synchronize_on(key) do + + @content[key] = value + + write_to_db(key, Marshal.dump(key), Marshal.dump(value)) + + return value + + end + end + # + # Stores whatever is under +key+ if it is not the same as + # whats in the persistent db. + # + def store_if_changed(key) + @lock.synchronize_on(key) do + + serialized_key = Marshal.dump(key) + serialized_value = Marshal.dump(@content[key]) + + write_to_db(key, serialized_key, serialized_value) if @content_db[serialized_key] != serialized_value + + end + end + # + # Returns the last time the value under +key+ was changed. + # + def timestamp(key) + @lock.synchronize_on(key) do + + timestamp = @timestamps[key] + return timestamp if timestamp + + serialized_key = Marshal.dump(key) + serialized_timestamp = @timestamps_db[serialized_key] + return nil unless serialized_timestamp + + timestamp = Marshal.load(serialized_timestamp) + @timestamps[key] = timestamp + return timestamp + + end + end + # + # Delete +key+ and its value and timestamp. + # + def delete(key) + @lock.synchronize_on(key) do + + serialized_key = Marshal.dump(key) + + @content.delete(key) + @content_db[serialized_key] = nil + @timestamps.delete(key) + @timestamps_db[serialized_key] = nil + + end + end + + private + + # + # Write +key+, serialized as +serialized_key+ and + # +serialized_value+ to the db. + # + def write_to_db(key, serialized_key, serialized_value) + now = Time.now + + @content_db[serialized_key] = serialized_value + @timestamps_db[serialized_key] = Marshal.dump(now) + @timestamps[key] = now + end + + # + # Read +key+ from db and if it is found + # put it in the cache Hash. + # + def get_from_db(key) + serialized_key = Marshal.dump(key) + serialized_value = @content_db[serialized_key] + return nil unless serialized_value + + value = Marshal.load(serialized_value) + @content[key] = value + return value + end + end + + # + # A simple persistence provider backed by Berkeley db. + # + class BerkeleyHashishProvider + # + # Initialize an instance with the given + # +env_path+ to its database dir. + # + def initialize(env_path) + env_path.mkpath + @env = BDB::Env.open(env_path, BDB::CREATE | BDB::INIT_MPOOL) + end + # + # Returns a cleverly cached (but slightly inefficient) + # hash-like instance (see Archipelago::Hashish::BerkeleyHashish) + # using +name+. + # + def get_cached_hashish(name) + BerkeleyHashish.new(name, @env) + end + # + # Returns a normal hash-like instance using +name+. + # + def get_hashish(name) + @env.open_db(BDB::HASH, name, nil, BDB::CREATE | BDB::NOMMAP) + end + # + # Removes the persistent files of this instance. + # + def unlink + p = Pathname.new(@env.home) + p.rmtree if p.exist? + end + end + + end + +end Copied: trunk/archipelago/lib/archipelago/pirate.rb (from rev 11, trunk/archipelago/lib/pirate.rb) =================================================================== --- trunk/archipelago/lib/archipelago/pirate.rb (rev 0) +++ trunk/archipelago/lib/archipelago/pirate.rb 2006-11-13 23:06:06 UTC (rev 12) @@ -0,0 +1,229 @@ +# Archipelago - a distributed computing toolkit for ruby +# Copyright (C) 2006 Martin Kihlgren +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'archipelago/disco' +require 'archipelago/tranny' +require 'archipelago/treasure' +require 'pp' +require 'drb' +require 'digest/sha1' + +module Archipelago + + # + # The client library that knows about + # all remote databases and handles reads + # and write to them. + # + module Pirate + + # + # Raised when you try to begin a transaction but have no managers + # available. + # + class NoTransactionManagerAvailableException < RuntimeError + def initialize(pirate) + super("#{pirate} can not find any transaction manager for you") + end + end + + # + # Raised when you try to do stuff without any remote database + # available. + # + class NoRemoteDatabaseAvailableException < RuntimeError + def initialize(pirate) + super("#{pirate} can not find any remote database for you") + end + end + + INITIAL_SERVICE_UPDATE_INTERVAL = 1 + MAXIMUM_SERVICE_UPDATE_INTERVAL = 60 + CHEST_DESCRIPTION = { + :class => 'Archipelago::Treasure::Chest' + } + TRANNY_DESCRIPTION = { + :class => 'Archipelago::Tranny::Manager' + } + + # + # The class that actually keeps track of the Archipelago::Treasure:Chests and the + # Archipelago::Treasure:Dubloons in them. + # + class Captain + attr_reader :chests, :treasure_map, :trannies, :transaction + # + # Initialize an instance using an Archipelago::Disco::Jockey with :jockey_options + # if given, that looks for new services :initial_service_update_interval or INITIAL_SERVICE_UPDATE_INTERVAL, + # when it starts and never slower than every :maximum_service_update_interval or MAXIMUM_SERVICE_UPDATE_INTERVAL. + # + # Will look for Archipelago::Treasure::Chests matching :chest_description or CHEST_DESCRIPTION and + # Archipelago::Tranny::Managers matching :tranny_description or TRANNY_DESCRIPTION. + # + def initialize(options = {}) + @treasure_map = Archipelago::Disco::Jockey.new(options[:jockey_options] || {}) + + @chest_description = CHEST_DESCRIPTION.merge(options[:chest_description] || {}) + @tranny_description = TRANNY_DESCRIPTION.merge(options[:tranny_description] || {}) + @jockey_options = options[:jockey_options] || {} + + start_service_updater(options[:initial_service_update_interval] || INITIAL_SERVICE_UPDATE_INTERVAL, + options[:maximum_service_update_interval] || MAXIMUM_SERVICE_UPDATE_INTERVAL) + + @yar_counter = 0 + + @transaction = nil + end + + # + # Get a value from the distributed database network using a +key+, + # optionally within a +transaction+. + # + def [](key, transaction = nil) + responsible_chest(key)[:service][key, transaction || @transaction] + end + + # + # Write a value to the distributed database network, + # optionally inside a transaction. + # + # Usage: + # * p["my key", transaction] = value + # * p["my key"] = value + # + def []=(key, p1, p2 = nil) + if @transaction && p2.nil? + p2 = p1 + p1 = @transaction + end + responsible_chest(key)[:service][key, p1] = p2 + end + + # + # Delete a value from the distributed database network, + # optionally inside a transaction. + # + def delete(key, transaction = nil) + responsible_chest(key)[:service].delete(key, transaction || @transaction) + end + + # + # Return a clone of this instance bound to a newly created transaction. + # + def begin + raise NoTransactionManagerAvailableException.new(self) if @trannies.empty? + + rval = self.clone + rval.instance_eval do + @transaction = @trannies.values.first[:service].begin + end + + return rval + end + + # + # Execute +block+ within a transaction. + # + # Will commit! transaction after the block is finished unless + # the transaction is aborted or commited already. + # + # Will abort! the transaction if any exception is raised. + # + def transaction(&block) #:yields: transaction + raise NoTransactionManagerAvailableException.new(self) if @trannies.empty? + + @transaction = @trannies.values.first[:service].begin + begin + yield(@transaction) + @transaction.commit! if @transaction.state == :active + rescue Exception => e + @transaction.abort! unless @transaction.state == :aborted + puts e + pp e.backtrace + ensure + @transaction = nil + end + end + + # + # Commit the transaction we are a member of and forget about it. + # + def commit! + @transaction.commit! + @transaction = nil + end + + # + # Abort the transaction we are a member of and forget about it. + # + def abort! + @transaction.abort! + @transaction = nil + end + + # + # Yarrr! + # + def yar! + @yar_counter += 1 + 'yar!' + end + + private + + # + # Get the chest responsible for +key+. + # + def responsible_chest(key) + raise NoRemoteDatabaseAvailableException.new(self) if @chests.empty? + + key_id = Digest::SHA1.new(Marshal.dump(key)).to_s + sorted_chest_ids = @chests.keys.sort + sorted_chest_ids.each do |id| + return @chests[id] if id > key_id + end + return @chests[sorted_chest_ids.first] + end + + # + # Start a thread looking up existing chests between every + # +initial+ and +maximum+ seconds. + # + def start_service_updater(initial, maximum) + @chests = @treasure_map.lookup(Archipelago::Disco::Query.new(@chest_description), 0) + @trannies = @treasure_map.lookup(Archipelago::Disco::Query.new(@tranny_description), 0) + Thread.start do + standoff = initial + loop do + begin + sleep(standoff) + standoff *= 2 + standoff = maximum if standoff > maximum + @chests = @treasure_map.lookup(Archipelago::Disco::Query.new(@chest_description), 0) + @trannies = @treasure_map.lookup(Archipelago::Disco::Query.new(@tranny_description), 0) + rescue Exception => e + puts e + pp e.backtrace + end + end + end + end + end + + end + +end Copied: trunk/archipelago/lib/archipelago/tranny.rb (from rev 11, trunk/archipelago/lib/tranny.rb) =================================================================== --- trunk/archipelago/lib/archipelago/tranny.rb (rev 0) +++ trunk/archipelago/lib/archipelago/tranny.rb 2006-11-13 23:06:06 UTC (rev 12) @@ -0,0 +1,651 @@ +# Archipelago - a distributed computing toolkit for ruby +# Copyright (C) 2006 Martin Kihlgren +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'bdb' +require 'pathname' +require 'drb' +require 'archipelago/current' +require 'digest/sha1' +require 'archipelago/disco' +require 'archipelago/hashish' + +module Archipelago + + module Tranny + + # + # Time before any Transaction will be automatically aborted. + # + TRANSACTION_TIMEOUT = 60 * 60 + + # + # If a member tries to join a transaction that it has allready joined + # it will receive this. + # + class JoinCountException < RuntimeError + def initialize(member, transaction) + super("#{member.inspect} has allready joined the transaction #{transaction.inspect}") + end + end + + # + # If a member tries to commit a transaction not in :active state + # or abort a transaction in :commited state, or any other grave + # state offence + # + class IllegalOperationException < RuntimeError + def initialize(operation, transaction) + super("#{operation.inspect} is illegal for #{transaction.inspect}") + end + end + + # + # If a member tries to get a transaction that the manager doesnt know about + # it gets this. + # + class UnknownTransactionException < RuntimeError + def initialize(transaction_id, manager) + super("#{manager.inspect} doesnt know about any transaction with id #{transaction_id.inspect}") + end + end + + # + # If a member tries to report its state and the manager doesnt know about the + # member, it gets this. + # + class UnknownMemberException < RuntimeError + def initialize(member, transaction) + super("#{transaction.inspect} doesnt know about any member like #{member.inspect}") + end + end + + # + # If a member misbehaves (or the Transaction is completely fucked up) this will be raised + # + class IllegalStateException < RuntimeError + def initialize(transaction) + super("#{transaction.inspect} is in a completely fucked up state") + end + end + + # + # The manager itself. + # + # This will be the drb exported object that participants talk to, + # either directly or through a TransactionProxy. + # + # See also the TransactionProxy and Transaction classes. + # + class Manager + + include DRb::DRbUndumped + include Archipelago::Disco::Publishable + + attr_accessor :error_logger + attr_accessor :transaction_timeout + + # + # Will use a BerkeleyHashishProvider using tranny_manager.db in the same dir to get its hashes + # if not :persistence_provider is given. + # + # Will create Transactions timing out after :transaction_timeout seconds or TRANSACTION_TIMEOUT + # if none is given. + # + # Will use Archipelago::Disco::Publishable by calling initialize_publishable with +options+. + # + def initialize(options = {}) + @persistence_provider = options[:persistence_provider] || Archipelago::Hashish::BerkeleyHashishProvider.new(Pathname.new(File.expand_path(__FILE__)).parent.join("tranny_manager.db")) + + initialize_publishable(options) + + @transaction_timeout = options[:transaction_timeout] || TRANSACTION_TIMEOUT + + @metadata = @persistence_provider.get_hashish("metadata") + @db = @persistence_provider.get_cached_hashish("db") + end + + # + # Returns a proxy to a newly created Transaction. + # + # The Transaction will timeout after :timeout seconds + # or @transaction_timeout if none is given. + # + def begin(options = {}) + options[:manager] = self + options[:timeout] ||= @transaction_timeout + return Transaction.new(options).proxy + end + + # + # Used by transactions to notify this manager + # about an error. Delegates the actual logging + # to @error_logger.call(exception). + # + # Set @error_logger or override this method + # if you want to log any errors. + # + def log_error(exception) + self.error_logger.call(exception) if self.error_logger + end + + # + # Used by a +transaction+ to store its state for future reference. + # + def store_transaction!(transaction) + @db[transaction.transaction_id] = transaction + end + + # + # Used by a +transaction+ to remove its state when finished. + # + def remove_transaction!(transaction) + @db.delete(transaction.transaction_id) + end + + # + # Used by a transaction proxy to run +meth+ with +args+ on its +transaction_id+. + # + def call_instance_method(transaction_id, meth, *args) + transaction = @db[transaction_id] + if transaction + return transaction.send(meth, *args) + else + raise UnknownTransactionException.new(transaction_id, self) + end + end + + end + + + + + # + # A proxy to a transaction managed by this manager. + # + # Used because standard DRbObjects only use object_id + # to identify objects, while we want the more unique transaction_id. + # + # Will also remember the state of the transaction when it detects + # methods that will terminate it (:commit | :abort). + # + class TransactionProxy + attr_accessor :transaction_id + def initialize(transaction) + @manager = transaction.manager + @transaction_id = transaction.transaction_id + @state = :unknown + end + # + # Return the cached state of the wrapped Archipelago::Tranny::Transaction + # if it is known, otherwise fetch it. + # + def state + if @state == :unknown + method_missing(:state) + else + @state + end + end + # + # Implemented to ensure that all TransactionProxies with the same + # transaction_id are hashwise equal. + # + def hash + @transaction_id.hash + end + # + # Implemented to ensure that all TransactionProxies with the same + # transaction_id are hashwise equal. + # + def eql?(o) + if TransactionProxy === o + o.transaction_id == @transaction_id + else + false + end + end + # + # Forwards everything to our Transaction and remembers + # returnvalue if necessary. + # + def method_missing(meth, *args) #:nodoc: + rval = @manager.call_instance_method(@transaction_id, meth, *args) + case meth + when :abort! + @state = :aborted + when :commit! + @state = rval + end + return rval + end + end + + + + + # + # A transaction managed by the manager. + # + # A transaction can have the following states: + # + # * :active - When it is first created. + # * This transaction can be commited or aborted. + # + # * :voting - When it has started the two-phase commit and the voting has begun. + # * This transaction can be aborted. + # + # * :commited - After everyone has voted and voted either :unchanged or :commit. + # * This transaction can not be changed. + # + # * :aborted - After someone has called abort! or voted :abort in a vote! + # * This transaction can not be changed. + # + # Anyone that wants to join the transaction must implement the following methods: + # + # * abort!(transaction): Abort the provided transaction. + # * commit!(transaction): Commit the provided transaction. + # * prepare!(transaction): Prepare for commiting the provided transaction. + # + # abort! and commit! should not return any values, but can raise exceptions + # if required. + # + # prepare! must return either :abort, :commit or :unchanged, depending on what + # the member is prepared to do. :unchanged is only when the member has not changed + # state during the transaction, and means that it does not require any further + # notification on the progress of the transaction. + # + # A member that has returned :commit on the prepare! must store the transaction + # proxy in a persistent manner to be able to connect to the manager + # and get a new copy of the transaction in case of communications failure. + # + # A member that reconnects to a crashed transaction manager should not abort! the + # transaction if the transaction is still in :active state, since the transaction will + # have been aborted on commit! anyway (since the manager will not be able to prepare! the + # disconnected member after either the manager or member having crashed) if needed. + # If the disconnect was just a temporary networking problem, the transaction will + # continue as planned. + # + # A member that reconnects to a crashed transaction manager where the transaction + # is in :voting state should just wait around and see if it gets prepare! called. + # In case of temporary network failure the transaction will continue as planned, otherwise + # it will abort! automatically. + # + # A member that reconnects to a disconnected transaction manager where the transaction + # is in :commited state should just commit its state. Then it must notify the transaction + # using report_commited! so that the transaction can disappear gracefully. + # + # A member that reconnects to a disconnected transaction manager that either doesnt know + # of the transaction or returns an :aborted transaction may safely abort the state change + # and forget about the transaction. + # + class Transaction + + include Archipelago::Current::Synchronized + + attr_reader :state, :transaction_id, :proxy, :manager + + # + # Create a transaction managed by the provided +manager+. + # + # Will have :manager as TransactionManager, and will timeout + # after :timeout seconds. + # + def initialize(options) + super() + # + # A hash where members are keys and their state the values. + # + @members = {} + @members.extend(Archipelago::Current::Synchronized) + # + # We are alive! + # + @state = :active + # + # We have a timeout! + # + @timeout = options[:timeout] + # + # We are unique! + # + @transaction_id = "#{options[:manager].service_id}:#{Time.new.to_f}:#{self.object_id}:#{rand(1 << 32)}" + # + # We have a birth time! + # + @created_at = Time.now + # + # We have a manager! + # + self.manager = options[:manager] + + store_us_for_future_reference! + + start_timeout_thread + end + + # + # Store this manager as ours and create a proxy that knows about it. + # + # Used by Archipelago::Tranny:Manager when restoring crashed + # Archipelago::Tranny:Transactions. + # + def manager=(manager) + # + # We have a manager! + # + @manager = DRbObject.new(manager) + # + # We have a proxy to send forth into the world! + # + @proxy = TransactionProxy.new(self) + nil + end + + # + # Special dump call to NOT dump our manager or proxy, + # since DRbObjects dont take kindly to being restored + # in an environment where they are are known to be invalid. + # + def _dump(l) + return Marshal.dump([ + @members, + @state, + @timeout, + @transaction_id, + @created_at + ]) + end + + def self._load(s) + members, state, timeout, transaction_id, created_at = Marshal.load(s) + rval = self.allocate + rval.instance_variable_set(:@members, members) + rval.instance_variable_set(:@state, state) + rval.instance_variable_set(:@timeout, timeout) + rval.instance_variable_set(:@transaction_id, transaction_id) + rval.instance_variable_set(:@created_at, created_at) + rval + end + + # + # Starts the thread that will abort! us automatically + # after we have lived @timeout + # + def start_timeout_thread + Thread.new do + now = Time.now + die_at = @created_at + @timeout + if die_at > now + sleep(die_at - now) + end + abort! + end + nil + end + + # + # What it sounds like. + # + def store_us_for_future_reference! + @manager.store_transaction!(self) + nil + end + + # + # Remove ourselves, we are redundant + # + def remove_us_we_are_redundant! + @manager.remove_transaction!(self) + nil + end + + # + # Used by members that failed during commit. + # + def report_commited!(member) + raise UnknownMemberException(member, self) unless @members.include?(member) + raise IllegalOperationException(:report_commited!, self) unless self.state == :commited + + @members[member] = :commited + + remove_us_if_all_are_commited! + nil + end + + # + # Abort the transaction, sending all participants the abort! message. + # + def abort! + synchronize do + raise IllegalOperationException.new(:abort!, self) if [:commited, :aborted].include?(self.state) + + # + # Set our state. + # + @state = :aborted + + store_us_for_future_reference! + + # + # Abort all members. + # + threads = [] + @members.clone.each do |member, state| + raise RuntimeException.new("This is not supposed to be possible, but we are in abort! with member " + + "#{member.inspect} in state #{state.inspect}") if state == :commited + if state != :aborted + threads << Thread.new do + begin + member.abort!(self.proxy) + @members.synchronize do + @members[member] = :aborted + end + rescue Exception => e + @manager.log_error(e) + # + # We must not let the other members stop just + # because one member failed. No more Mr Nice Guy! + # + end + end + end + end + + # + # Wait for all members to finish. + # + threads.each do |thread| + thread.join + end + + # + # No use having aborted transactions lying about. + # + # NB: This means that disconnected members that cant + # find their old transactions will have to presume they + # have been aborted. + # + remove_us_we_are_redundant! + end + nil + end + + # + # Commits the transaction, returning the new state (:aborted | :commited) + # + def commit! + synchronize do + raise IllegalOperationException.new(:commit!, self) unless self.state == :active + + # + # Vote for the outcome and act on it + # + case vote! + when :abort + abort! + when :commit + _commit! + when :unchanged + @state = :commited + remove_us_we_are_redundant! + end + + return @state + end + nil + end + + # + # Join a +member+ to this transaction. + # + # Will raise a JoinCountException if the given member has allready + # joined this transaction. + # + def join(member) + @members.synchronize do + raise IllegalOperationException.new(:join, self) unless self.state == :active + raise JoinCountException.new(member, self) if @members.include?(member) + + @members[member] = :active + end + store_us_for_future_reference! + nil + end + + private + + # + # Commit the transaction, sending all participants the commit message. + # + # The transaction is commited by all members having commit! called. + # + def _commit! + # + # Set our state. + # + @state = :commited + + store_us_for_future_reference! + + # + # Commit all members. + # + threads = [] + @members.clone.each do |member, state| + raise RuntimeException.new("This is not supposed to be possible, but we are in _commit with member " + + "#{member.inspect} in state #{state.inspect}") unless [:prepared, :commited].include?(state) + threads << Thread.new do + begin + member.commit!(self.proxy) + @members.synchronize do + @members[member] = :commited + end + rescue Exception => e + @manager.log_error(e) + # + # We must not let the other members stop just + # because one member failed. No more Mr Nice Guy! + # + end + end + end + + # + # Wait for all members to finish. + # + threads.each do |thread| + thread.join + end + + remove_us_if_all_are_commited! + end + + def remove_us_if_all_are_commited! + # + # Check to see if all members have been told about the decision. + # + all_have_commited = true + @members.each do |member, state| + all_have_commited = false unless state == :comitted + end + + # + # If they have, remove ourselves from the manager. + # + remove_us_we_are_redundant! if all_have_commited + end + + # + # Let the members of the transaction vote for its outcome. + # + # Members vote by having prepare! called. + # + # Valid returnvalues for the prepare! call are: + # :abort, if the member wants to abort the transaction + # :commit, if the member wants to commit the transaction + # :unchanged, if the member has not changed state during the transaction + # + def vote! + raise IllegalOperationException.new(:vote!, self) unless self.state == :active + + @state = :voting + + store_us_for_future_reference! + + return_value = :commit + + threads = [] + @members.clone.each do |member, state| + raise RuntimeException.new("This is not supposed to be possible, but we are in vote! with member " + + "#{member.inspect} in state #{state.inspect}") unless state == :active + threads << Thread.new do + this_result = nil + begin + this_result = member.prepare!(self.proxy) + rescue Exception => e + @manager.log_error(e) + this_result = :abort + end + @members.synchronize do + case this_result + when :abort + @members[member] = :aborted + return_value = :abort + when :unchanged + @members.delete(member) + else + @members[member] = :prepared + end + end + end + end + + threads.each do |thread| + thread.join + end + + if @members.empty? + return_value = :unchanged + end + + return_value + end + + end + end + +end Copied: trunk/archipelago/lib/archipelago/treasure.rb (from rev 11, trunk/archipelago/lib/treasure.rb) =================================================================== --- trunk/archipelago/lib/archipelago/treasure.rb (rev 0) +++ trunk/archipelago/lib/archipelago/treasure.rb 2006-11-13 23:06:06 UTC (rev 12) @@ -0,0 +1,672 @@ +# Archipelago - a distributed computing toolkit for ruby +# Copyright (C) 2006 Martin Kihlgren +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'drb' +require 'archipelago/disco' +require 'bdb' +require 'pathname' +require 'digest/sha1' +require 'pp' +require 'set' +require 'archipelago/hashish' +require 'archipelago/tranny' + +module Archipelago + + module Treasure + + # + # Minimum time between trying to recover + # crashed transactions. + # + TRANSACTION_RECOVERY_INTERVAL = 30 + + # + # Raised whenever the optimistic locking of the serializable transaction isolation level + # was proved wrong. + # + class RollbackException < RuntimeError + def initialize(chest, transaction) + super("#{chest} has been modified during #{transaction}") + end + end + + # + # Raised whenever a method is called on an object that we dont know about. + # + class UnknownObjectException < RuntimeError + def initialize(chest, key, transaction) + super("#{chest} does not contain #{key} under #{transaction}") + end + end + + # + # Raised if a Dubloon or Chest doesnt know what transaction you are talking about. + # + class UnknownTransactionException < RuntimeError + def initialize(source, transaction) + super("#{source} is not a part of #{transaction}") + end + end + + # + # Raised if anyone tries to commit a non-prepared transaction. + # + class IllegalCommitException < RuntimeError + def initialize(source, transaction) + super("#{transaction} is not prepared in #{source}") + end + end + + # + # Raised if someone tries to join us to a non-active transaction. + # + class IllegalJoinException < RuntimeError + def initialize(transaction) + super("#{transaction} is not active") + end + end + + # + # A proxy to something in the chest. + # + class Dubloon + # + # Remove all methods so that we look like our target. + # + instance_methods.each do |method| + undef_method method unless method =~ /^__/ + end + # + # Initialize us with knowledge of our +chest+, the +key+ to our + # target in the +chest+, the known +public_methods+ of our target + # and any +transaction+ we are associated with. + # + def initialize(key, chest, transaction, chest_id, public_methods) + @key = key + @chest = chest + @transaction = transaction + @chest_id = chest_id + @public_methods = public_methods + end + # + # The public_methods of our target. + # + def public_methods + return @public_methods.clone + end + # + # Return a clone of myself that is joined to + # the +transaction+. + # + def join(transaction) + @chest.join!(transaction) if transaction + return Dubloon.new(@key, @chest, transaction, @chest_id, @public_methods) + end + # + # Raises exception if the given +transaction+ + # is not the same as our own. + # + def assert_transaction(transaction) + raise UnknownTransactionException.new(self, transaction) unless transaction == @transaction + end + # + # The object_id of our chest-held target. + # + def object_id + id = "#{@chest_id}:#{@key}" + id << ":#{@transaction.transaction_id}" if @transaction + return id + end + # + # Does our target respond to +meth+? + # + def respond_to?(meth) + return @public_methods.include?(meth.to_sym) || @public_methods.include?(meth.to_s) + end + # + # Call +meth+ with +args+ and +block+ on our target if it responds to + # it. + # + def method_missing(meth, *args, &block) + if respond_to?(meth) + return @chest.call_instance_method(@key, meth, @transaction, *args, &block) + else + return super(meth, *args) + end + end + + end + + # + # A possibly remote database that only returns proxies to its + # contents, and thus runs all methods on its contents itself. + # + # Has support for optimistically locked distributed serializable transactions. + # + class Chest + + # + # The Chest never leaves its host. + # + include DRb::DRbUndumped + + # + # The Chest can be published. + # + include Archipelago::Disco::Publishable + + # + # Initialize a Chest + # + # Will use a BerkeleyHashishProvider using treasure_chest.db in the same dir to get its hashes + # if not :persistence_provider is given. + # + # Will try to recover crashed transaction every :transaction_recovery_interval seconds + # or TRANSACTION_RECOVERY_INTERVAL if none is given. + # + # Will use Archipelago::Disco::Publishable by calling initialize_publishable with +options+. + # + def initialize(options = {}) + # + # The provider of happy magic persistent hashes of different kinds. + # + @persistence_provider = options[:persistence_provider] || Archipelago::Hashish::BerkeleyHashishProvider.new(Pathname.new(File.expand_path(__FILE__)).parent.join("treasure_chest.db")) + + # + # Use the given options to initialize the publishable + # instance variables. + # + initialize_publishable(options) + + # + # [transaction => [key => instance]] + # To know what stuff is visible to a transaction. + # + @snapshot_by_transaction = {} + @snapshot_by_transaction.extend(Archipelago::Current::Synchronized) + + # + # [transaction => [key => when the key was read/updated/deleted] + # To know if a transaction is ok to prepare and commit. + # + @timestamp_by_key_by_transaction = {} + + # + # [transaction => [key => instance]] + # To know what transactions were prepared but not + # properly finished last run. + # + @crashed = Set.new + + # + # The magical persistent map that defines how we actually + # store our data. + # + @db = @persistence_provider.get_cached_hashish("db") + + initialize_prepared(options[:transaction_recovery_interval] || TRANSACTION_RECOVERY_INTERVAL) + + end + + # + # The transactions active in this Chest. + # + def active_transactions + @snapshot_by_transaction.keys.clone + end + + # + # Return the contents of this chest using a given +key+ and +transaction+. + # + def [](key, transaction = nil) + join!(transaction) + + instance = ensure_instance_with_transaction(key, transaction) + return nil unless instance + + if Dubloon === instance + return instance.join(transaction) + else + return Dubloon.new(key, DRbObject.new(self), transaction, self.service_id, instance.public_methods) + end + end + + # + # Delete the value of +key+ within +transaction+. + # + def delete(key, transaction = nil) + join!(transaction) + + rval = nil + + if transaction + # + # If we have a transaction we must note that it is deleted in a + # separate space for that transaction. + # + snapshot = @snapshot_by_transaction[transaction] + snapshot.synchronize do + + rval = snapshot[key] + + snapshot[key] = :deleted + # + # Make sure we remember when it was last changed according to our main db. + # + timestamps = @timestamp_by_key_by_transaction[transaction] + timestamps[key] = @db.timestamp(key) unless timestamps.include?(key) + + end + else + # + # Otherwise just ask our persistence provider to delete it. + # + @db.delete(key) + end + + rval.freeze + + return rval + end + + # + # Put something into this chest with a given +key+, +value+ and + # +transaction+. + # + def []=(key, p1, p2 = nil) + if p2 + value = p2 + transaction = p1 + else + value = p1 + transaction = nil + end + + return set(key, value, transaction) + end + + # + # Call an instance +method+ on whatever this chest holds at +key+ + # with any +transaction+ and +args+. + # + def call_instance_method(key, method, transaction, *arguments, &block) + if transaction + return call_with_transaction(key, method, transaction, *arguments, &block) + else + return call_without_transaction(key, method, *arguments, &block) + end + end + + # + # Abort +transaction+ in this Chest. + # + def abort!(transaction) + assert_transaction(transaction) + + snapshot = @snapshot_by_transaction[transaction] + # + # Make sure nobody can modify this transaction while we + # are aborting it. + # + snapshot.synchronize do + serialized_transaction = Marshal.dump(transaction) + + # + # If this transaction was successfully prepared + # + if @snapshot_by_transaction_db.include?(serialized_transaction) + # + # Unlock the keys that are part of it. + # + snapshot.each do |key, value| + @db.unlock_on(key) + end + # + # And remove it from persistent storage. + # + @snapshot_by_transaction_db[serialized_transaction] = nil + # + # And remove its timestamps from persistent storage. + # + @timestamp_by_key_by_transaction_db[serialized_transaction] = nil + end + # + # Finally delete it from the snapshots. + # + @snapshot_by_transaction.delete(transaction) + # + # And from the timestamps. + # + @timestamp_by_key_by_transaction.delete(transaction) + end + end + + # + # Prepares +transaction+ in this Chest. + # + # NB: This will cause any update of the data within + # this transaction to block until it is either aborted + # or commited! + # + def prepare!(transaction) + assert_transaction(transaction) + + # + # If we dont know about this transaction then it can't very well + # affect us. + # + return :unchanged unless @snapshot_by_transaction.include?(transaction) + + snapshot = @snapshot_by_transaction[transaction] + # + # Make sure nobody can modify this transaction while we are + # preparing it. + # + snapshot.synchronize do + + # + # Remember what locks we acquire so that we can + # unlock them in case of failure. + # + locks = [] + timestamp_by_key = @timestamp_by_key_by_transaction[transaction] + # + # Acquire a lock on each key in the transaction + # + snapshot.each do |key, value| + if @db.timestamp(key) == timestamp_by_key[key] + @db.lock_on(key) + locks << key + else + locks.each do |key| + @db.unlock_on(key) + end + return :abort + end + end + serialized_transaction = Marshal.dump(transaction) + + # + # Dump its state to persistent storage. + # + @snapshot_by_transaction_db[serialized_transaction] = Marshal.dump(snapshot) + # + # And dump its timestamps to persistent storage + # + @timestamp_by_key_by_transaction_db[serialized_transaction] = Marshal.dump(timestamp_by_key) + return :prepared + end + end + + # + # Commits +transaction+ in this Chest. + # + # NB: Transaction must be prepared before commit is called. + # + def commit!(transaction) + assert_transaction(transaction) + raise IllegalCommitException.new(self, transaction) unless @snapshot_by_transaction_db.include?(Marshal.dump(transaction)) + + snapshot = @snapshot_by_transaction[transaction] + # + # Make sure nobody can modify this transaction while we are + # commiting it. + # + snapshot.synchronize do + + # + # Copy each key and value from our private space to the real space + # + snapshot.each do |key, value| + if value == :deleted + @db.delete(key) + else + @db[key] = value + end + end + + # + # Call abort! to clean up after the transaction. + # + abort!(transaction) + + end + end + + private + + # + # Allocates space for this +transaction+. + # + # Will also call +transaction+.join to make sure + # it is aware of us. + # + def join!(transaction) + if transaction + if transaction.state == :active + @snapshot_by_transaction.synchronize do + unless @snapshot_by_transaction.include?(transaction) + @snapshot_by_transaction[transaction] = {} + @snapshot_by_transaction[transaction].extend(Archipelago::Current::Synchronized) + @timestamp_by_key_by_transaction[transaction] = {} + transaction.join(DRbObject.new(self)) + end + end + else + raise IllegalJoinException.new(transaction) + end + end + end + + # + # Raises if we are not in this transaction. + # + def assert_transaction(transaction) + raise UnknownTransactionException.new(self, transaction) unless @snapshot_by_transaction.include?(transaction) + end + + # + # Call a method within a transaction. + # + def call_with_transaction(key, method, transaction, *arguments, &block) + assert_transaction(transaction) + + # + # Fetch our instance from the snapshot. + # + snapshot = @snapshot_by_transaction[transaction] + instance = snapshot[key] + instance = nil if instance == :deleted + + raise UnknownObjectException.new(self, key, transaction) unless instance + + begin + return execute(instance, method, *arguments, &block) + ensure + # + # Make sure we remember when this object was last changed according + # to the main db. + # + snapshot.synchronize do + timestamps = @timestamp_by_key_by_transaction[transaction] + timestamps[key] = @db.timestamp(key) unless timestamps.include?(key) + end + end + end + + # + # Execute +m+ with arguments +a+ and block +b+ on +o+. + # + def execute(o, m, *a, &b) + if b + return o.send(m, *a, &b) + else + return o.send(m, *a) + end + end + + # + # Call a method outside any transaction (ie inside a transaction of its own). + # + def call_without_transaction(key, method, *arguments, &block) + instance = @db[key] + + raise UnknownObjectException(self, key, transaction) unless instance + + begin + return execute(instance, method, *arguments, &block) + ensure + @db.store_if_changed(key) + end + end + + # + # Initializes our storage of prepared transactions. + # + def initialize_prepared(transaction_recovery_interval) + # + # Load stored timestamps for our transaction from db. + # + @timestamp_by_key_by_transaction_db = @persistence_provider.get_hashish("prepared_timestamps") + @timestamp_by_key_by_transaction_db.each do |serialized_transaction, serialized_timestamps| + @timestamp_by_key_by_transaction[Marshal.load(serialized_transaction)] = Marshal.load(serialized_timestamps) + end + + # + # Load stored snapshots for our transaction from db. + # + @snapshot_by_transaction_db = @persistence_provider.get_hashish("prepared") + @snapshot_by_transaction_db.each do |serialized_transaction, serialized_snapshot| + transaction = Marshal.load(transaction) + + @crashed << transaction + @snapshot_by_transaction[transaction] = Marshal.load(serialized_snapshot) + end + start_recovery_thread(transaction_recovery_interval) + end + + # + # Starts the thread that will keep trying to recover + # our crashed transactions. + # + def start_recovery_thread(transaction_recovery_interval) + Thread.new do + loop do + begin + @crashed.clone.each do |transaction| + begin + case transaction.state + when :commited + commit!(transaction) + @crashed.delete(transaction) + when :aborted + abort!(transaction) + @crashed.delete(transaction) + end + rescue Archipelago::Tranny::UnknownTransactionException => e + abort!(transaction) + @crashed.delete(transaction) + end + end + sleep(transaction_recovery_interval) + rescue Exception => e + puts e + pp e.backtrace + end + end + end + end + + # + # Insert +value+ under +key+ and +transaction+ + # in this chest. + # + def set(key, value, transaction) + join!(transaction) + value.assert_transaction(transaction) if Dubloon === value + + if transaction + snapshot = @snapshot_by_transaction[transaction] + + # + # If we have a transaction we must put it in a + # separate space for that transaction. + # + snapshot.synchronize do + + snapshot[key] = value + # + # Make sure we remember the last time this was changed according to + # our main db. + # + timestamps = @timestamp_by_key_by_transaction[transaction] + timestamps[key] = @db.timestamp(key) unless timestamps.include?(key) + + end + else + @db[key] = value + end + + return value if Dubloon === value + + return Dubloon.new(key, DRbObject.new(self), transaction, service_id, value.public_methods) + end + + # + # Try to fetch the data of +key+ from the private space + # of +transaction+ and put it there if it was not there + # already. + # + def ensure_instance_with_transaction(key, transaction) + if transaction + snapshot = @snapshot_by_transaction[transaction] + snapshot.synchronize do + + # + # If we dont have this key in the snapshot. + # + unless snapshot.include?(key) + # + # Fetch the new value for the snapshot + # + new_value = @db.get_deep_clone(key) + # + # If it exists then copy it to the snapshot + # otherwise remove the transaction hash if it is empty. + # + if new_value + snapshot[key] = new_value + else + @snapshot_by_transaction.delete(transaction) if snapshot.empty? + end + end + + rval = snapshot[key] + return rval == :deleted ? nil : rval + + end + else + return @db[key] + end + end + + end + + end + +end Deleted: trunk/archipelago/lib/disco.rb =================================================================== --- trunk/archipelago/lib/disco.rb 2006-11-13 23:04:26 UTC (rev 11) +++ trunk/archipelago/lib/disco.rb 2006-11-13 23:06:06 UTC (rev 12) @@ -1,548 +0,0 @@ -# Archipelago - a distributed computing toolkit for ruby -# Copyright (C) 2006 Martin Kihlgren -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -require 'socket' -require 'thread' -require 'ipaddr' -require 'pp' -require 'archipelago/current' -require 'drb' -require 'set' - -module Archipelago - - module Disco - - # - # Default address to use. - # - ADDRESS = "234.2.4.2" - # - # Default port to use. - # - PORT = 25242 - # - # Default port range to use for unicast. - # - UNIPORTS = 25243..26243 - # - # Default lookup timeout. - # - LOOKUP_TIMEOUT = 10 - # - # Default initial pause between resending lookup queries. - # Will be doubled for each resend. - # - INITIAL_LOOKUP_STANDOFF = 0.1 - # - # Default pause between trying to validate all services we - # know about. - # - VALIDATION_INTERVAL = 60 - # - # Only save stuff that we KNOW we want. - # - THRIFTY_CACHING = true - # - # Only reply to the one actually asking about a service. - # - THRIFTY_REPLYING = true - # - # Dont send on publish, only on query. - # - THRIFTY_PUBLISHING = false - - # - # A module to simplify publishing services. - # - # If you include it you can use the publish! method - # at your convenience. - # - # If you want to customize the publishing related behaviour you can - # call initialize_publishable with a Hash of options. - # - # See Archipelago::Treasure::Chest or Archipelago::Tranny::Manager for examples. - # - # It will store the service_id of this service in a directory beside this - # file (publishable.rb) named as the class you include into unless you - # define @persistence_provider before you call initialize_publishable. - # - module Publishable - - # - # Will initialize this instance with @service_description and @jockey_options - # and merge these with the optionally given :service_description and - # :jockey_options. - # - def initialize_publishable(options = {}) - @service_description = { - :service_id => service_id, - :validator => DRbObject.new(self), - :service => DRbObject.new(self), - :class => self.class.name - }.merge(options[:service_description] || {}) - @jockey_options = options[:jockey_options] || {} - end - - # - # Create an Archipelago::Disco::Jockey for this instance using @jockey_options - # or optionally given :jockey_options. - # - # Will publish this service using @service_description or optionally given - # :service_description. - # - def publish!(options = {}) - @jockey ||= Archipelago::Disco::Jockey.new(@jockey_options.merge(options[:jockey_options] || {})) - @jockey.publish(Archipelago::Disco::Record.new(@service_description.merge(options[:service_description] || {}))) - end - - # - # We are always valid if we are able to reply. - # - def valid? - true - end - - # - # Returns our semi-unique id so that we can be found again. - # - def service_id - # - # The provider of happy magic persistent hashes of different kinds. - # - @persistence_provider ||= Archipelago::Hashish::BerkeleyHashishProvider.new(Pathname.new(File.expand_path(__FILE__)).parent.join(self.class.name + ".db")) - # - # Stuff that didnt fit in any of the other databases. - # - @metadata ||= @persistence_provider.get_hashish("metadata") - service_id = @metadata["service_id"] - unless service_id - host = "#{Socket::gethostbyname(Socket::gethostname)[0]}" rescue "localhost" - service_id = @metadata["service_id"] ||= Digest::SHA1.hexdigest("#{host}:#{Time.new.to_f}:#{self.object_id}:#{rand(1 << 32)}") - end - return service_id - end - - end - - # - # A mock validator to be used for dumb systems that dont want - # to validate. - # - class MockValidator - def valid? - true - end - end - - # - # A Hash-like description of a service. - # - class ServiceDescription - IGNORABLE_ATTRIBUTES = Set[:unicast_reply] - attr_reader :attributes - # - # Initialize this service description with a hash - # that describes its attributes. - # - def initialize(hash = {}) - @attributes = hash - end - # - # Forwards as much as possible to our Hash. - # - def method_missing(meth, *args, &block) - if @attributes.respond_to?(meth) - if block - @attributes.send(meth, *args, &block) - else - @attributes.send(meth, *args) - end - else - super(*args) - end - end - # - # Returns whether this ServiceDescription matches the given +match+. - # - def matches?(match) - match.each do |key, value| - unless IGNORABLE_ATTRIBUTES.include?(key) - return false unless @attributes.include?(key) && (value.nil? || @attributes[key] == value) - end - end - true - end - end - - # - # A class used to query the Disco network for services. - # - class Query < ServiceDescription - end - - # - # A class used to define an existing service. - # - class Record < ServiceDescription - # - # Initialize this Record with a hash that must contain an :service_id and a :validator. - # - def initialize(hash) - raise "Record must have an :service_id" unless hash.include?(:service_id) - raise "Record must have a :validator" unless hash.include?(:validator) - super(hash) - end - # - # Returns whether this service is still valid. - # - def valid? - begin - self[:validator].valid? - rescue DRb::DRbError => e - false - end - end - end - - # - # A container of services. - # - class ServiceLocker - attr_reader :hash - include Archipelago::Current::Synchronized - def initialize(hash = nil) - super - @hash = hash || {} - end - # - # Merge this locker with another. - # - def merge(sd) - rval = @hash.clone - rval.merge!(sd.hash) - ServiceLocker.new(rval) - end - # - # Forwards as much as possible to our Hash. - # - def method_missing(meth, *args, &block) - if @hash.respond_to?(meth) - synchronize do - if block - @hash.send(meth, *args, &block) - else - @hash.send(meth, *args) - end - end - else - super(*args) - end - end - # - # Find all containing services matching +match+. - # - def get_services(match) - rval = ServiceLocker.new - self.each do |service_id, service_data| - rval[service_id] = service_data if service_data.matches?(match) && service_data.valid? - end - return rval - end - # - # Remove all non-valid services. - # - def validate! - self.clone.each do |service_id, service_data| - self.delete(service_id) unless service_data.valid? - end - end - end - - # - # The main discovery class used to both publish and lookup services. - # - class Jockey - - attr_reader :new_service_semaphore - - # - # Will create a Jockey service running on :address and :port or - # ADDRESS and PORT if none are given. - # - # Will the first available unicast port within :uniports or if not given UNIPORTS for receiving unicast messages. - # - # Will have a default :lookup_timeout of LOOKUP_TIMEOUT, a default - # :initial_lookup_standoff of INITIAL_LOOKUP_STANDOFF and a default - # :validation_interval of VALIDATION_INTERVAL. - # - # Will only cache (and validate, which saves network traffic) stuff - # that has been looked up before if :thrifty_caching, or THRIFTY_CACHING if not given. - # - # Will only reply to the one that sent out the query (and therefore save lots of network traffic) - # if :thrifty_replying, or THRIFTY_REPLYING if not given. - # - # Will send out a multicast when a new service is published unless :thrifty_publishing, or - # THRIFTY_PUBLISHING if not given. - # - # Will reply to all queries to which it has matching local services with a unicast message if :thrifty_replying, - # or if not given THRIFTY_REPLYING. Otherwise will reply with multicasts. - # - def initialize(options = {}) - @thrifty_caching = options.include?(:thrifty_caching) ? options[:thrifty_caching] : THRIFTY_CACHING - @thrifty_replying = options.include?(:thrifty_replying) ? options[:thrifty_replying] : THRIFTY_REPLYING - @thrifty_publishing = options.include?(:thrifty_publishing) ? options[:thrifty_publishing] : THRIFTY_PUBLISHING - @lookup_timeout = options[:lookup_timeout] || LOOKUP_TIMEOUT - @initial_lookup_standoff = options[:initial_lookup_standoff] || INITIAL_LOOKUP_STANDOFF - - @remote_services = ServiceLocker.new - @local_services = ServiceLocker.new - @subscribed_services = Set.new - - @incoming = Queue.new - @outgoing = Queue.new - - @new_service_semaphore = MonitorMixin::ConditionVariable.new(Archipelago::Current::Lock.new) - - @listener = UDPSocket.new - @unilistener = UDPSocket.new - - @listener.setsockopt(Socket::IPPROTO_IP, - Socket::IP_ADD_MEMBERSHIP, - IPAddr.new(options[:address] || ADDRESS).hton + Socket.gethostbyname("0.0.0.0")[3]) - - @listener.setsockopt(Socket::SOL_SOCKET, - Socket::SO_REUSEADDR, - true) - begin - @listener.setsockopt(Socket::SOL_SOCKET, - Socket::SO_REUSEPORT, - true) - rescue - # /moo - end - @listener.bind('', options[:port] || PORT) - - uniports = options[:uniports] || UNIPORTS - this_port = uniports.min - begin - @unilistener.bind('', this_port) - rescue Errno::EADDRINUSE => e - if this_port < uniports.max - this_port += 1 - retry - else - raise e - end - end - @unicast_address = "#{Socket::gethostbyname(Socket::gethostname)[0]}:#{this_port}" rescue "localhost:#{this_port}" - - @sender = UDPSocket.new - @sender.connect(options[:address] || ADDRESS, options[:port] || PORT) - - @unisender = UDPSocket.new - - start_listener - start_unilistener - start_shouter - start_picker - start_validator(options[:validation_interval] || VALIDATION_INTERVAL) - end - - # - # Stops all the threads in this instance. - # - def stop - @listener_thread.kill - @unilistener_thread.kill - @shouter_thread.kill - @picker_thread.kill - @validator_thread.kill - end - - # - # Lookup any services matching +match+, optionally with a +timeout+. - # - # Will immediately return if we know of matching and valid services, - # will otherwise send out regular Queries and return as soon as - # matching services are found, or when the +timeout+ runs out. - # - def lookup(match, timeout = @lookup_timeout) - match[:unicast_reply] = @unicast_address - @subscribed_services << match if @thrifty_caching - standoff = @initial_lookup_standoff - - @outgoing << [nil, match] - known_services = @remote_services.get_services(match).merge(@local_services.get_services(match)) - return known_services unless known_services.empty? - - @new_service_semaphore.wait(standoff) - standoff *= 2 - - t = Time.new - while Time.new < t + timeout - known_services = @remote_services.get_services(match).merge(@local_services.get_services(match)) - return known_services unless known_services.empty? - - @new_service_semaphore.wait(standoff) - standoff *= 2 - - @outgoing << [nil, match] - end - - ServiceLocker.new - end - - # - # Record the given +service+ and broadcast about it. - # - def publish(service) - if service.valid? - @local_services[service[:service_id]] = service - @new_service_semaphore.broadcast - unless @thrifty_publishing - @outgoing << [nil, service] - end - end - end - - private - - # - # Start the validating thread. - # - def start_validator(validation_interval) - @validator_thread = Thread.new do - loop do - begin - @local_services.validate! - @remote_services.validate! - sleep(validation_interval) - rescue Exception => e - puts e - pp e.backtrace - end - end - end - end - - # - # Start the thread sending Records and Queries - # - def start_shouter - @shouter_thread = Thread.new do - loop do - begin - recipient, data = @outgoing.pop - if recipient - address, port = recipient.split(/:/) - @unisender.send(Marshal.dump(data), 0, address, port.to_i) - else - begin - @sender.write(Marshal.dump(data)) - rescue Errno::ECONNREFUSED => e - retry - end - end - rescue Exception => e - puts e - pp e.backtrace - end - end - end - end - - # - # Start the thread receiving Records and Queries - # - def start_listener - @listener_thread = Thread.new do - loop do - begin - @incoming << Marshal.load(@listener.recv(1024)) - rescue Exception => e - puts e - pp e.backtrace - end - end - end - end - - # - # Start the thread receiving Records and Queries - # on unicast. - # - def start_unilistener - @unilistener_thread = Thread.new do - loop do - begin - @incoming << Marshal.load(@unilistener.recv(1024)) - rescue Exception => e - puts e - pp e.backtrace - end - end - end - end - - # - # Start the thread picking incoming Records and Queries and - # handling them properly - # - def start_picker - @picker_thread = Thread.new do - loop do - begin - data = @incoming.pop - if Archipelago::Disco::Query === data - @local_services.get_services(data).each do |service_id, service_data| - if @thrifty_replying - @outgoing << [data[:unicast_reply], service_data] - else - @outgoing << [nil, service_data] - end - end - elsif Archipelago::Disco::Record === data - if interesting?(data) && data.valid? - @remote_services[data[:service_id]] = data - @new_service_semaphore.broadcast - end - end - rescue Exception => e - puts e - pp e.backtrace - end - end - end - end - - # - # Are we generous in our caching, or have we been - # asked about this type of +publish+ before? - # - def interesting?(publish) - @subscribed_services.each do |subscribed| - return true if publish.matches?(subscribed) - end - return !@thrifty_caching - end - - end - - end - -end Deleted: trunk/archipelago/lib/hashish.rb =================================================================== --- trunk/archipelago/lib/hashish.rb 2006-11-13 23:04:26 UTC (rev 11) +++ trunk/archipelago/lib/hashish.rb 2006-11-13 23:06:06 UTC (rev 12) @@ -1,199 +0,0 @@ -# Archipelago - a distributed computing toolkit for ruby -# Copyright (C) 2006 Martin Kihlgren -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is