# Phusion Passenger - http://www.modrails.com/ # Copyright (C) 2008 Phusion # # Phusion Passenger is a trademark of Hongli Lai & Ninh Bui. # # 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; version 2 of the License. # # 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 'phusion_passenger/utils' module PhusionPassenger # This class maintains a collection of AbstractServer objects. One can add new # AbstractServer objects, or look up existing ones via a key. # AbstractServerCollection also automatically takes care of cleaning up # AbstractServers that have been idle for too long. # # This class exists because both SpawnManager and Railz::FrameworkSpawner need this kind # of functionality. SpawnManager maintains a collection of Railz::FrameworkSpawner # and Railz::ApplicationSpawner objects, while Railz::FrameworkSpawner maintains a # collection of Railz::ApplicationSpawner objects. # # This class is thread-safe as long as the specified thread-safety rules are followed. class AbstractServerCollection attr_reader :next_cleaning_time include Utils def initialize @collection = {} @lock = Mutex.new @cleanup_lock = Mutex.new @cond = ConditionVariable.new @done = false # The next time the cleaner thread should check for idle servers. # The value may be nil, in which case the value will be calculated # at the end of the #synchronized block. # # Invariant: # if value is not nil: # There exists an s in @collection with s.next_cleaning_time == value. # for all s in @collection: # if eligable_for_cleanup?(s): # s.next_cleaning_time <= value @next_cleaning_time = Time.now + 60 * 60 @next_cleaning_time_changed = false @cleaner_thread = Thread.new do begin @lock.synchronize do cleaner_thread_main end rescue Exception => e print_exception(self.class.to_s, e) end end end # Acquire the lock for this AbstractServerCollection object, and run # the code within the block. The entire block will be a single atomic # operation. def synchronize @lock.synchronize do yield if @next_cleaning_time.nil? @collection.each_value do |server| if @next_cleaning_time.nil? || (eligable_for_cleanup?(server) && server.next_cleaning_time < @next_cleaning_time ) @next_cleaning_time = server.next_cleaning_time end end if @next_cleaning_time.nil? # There are no servers in the collection with an idle timeout. @next_cleaning_time = Time.now + 60 * 60 end @next_cleaning_time_changed = true end if @next_cleaning_time_changed @next_cleaning_time_changed = false @cond.signal end end end # Lookup and returns an AbstractServer with the given key. # # If there is no AbstractSerer associated with the given key, then the given # block will be called. That block must return an AbstractServer object. Then, # that object will be stored in the collection, and returned. # # The block must set the 'max_idle_time' attribute on the AbstractServer. # AbstractServerCollection's idle cleaning interval will be adapted to accomodate # with this. Changing the value outside this block is not guaranteed to have any # effect on the idle cleaning interval. # A max_idle_time value of nil or 0 means the AbstractServer will never be idle cleaned. # # If the block raises an exception, then the collection will not be modified, # and the exception will be propagated. # # Precondition: this method must be called within a #synchronize block. def lookup_or_add(key) raise ArgumentError, "cleanup() has already been called." if @done server = @collection[key] if server register_activity(server) return server else server = yield if !server.respond_to?(:start) raise TypeError, "The block didn't return a valid AbstractServer object." end if eligable_for_cleanup?(server) server.next_cleaning_time = Time.now + server.max_idle_time if @next_cleaning_time && server.next_cleaning_time < @next_cleaning_time @next_cleaning_time = server.next_cleaning_time @next_cleaning_time_changed = true end end @collection[key] = server return server end end # Checks whether there's an AbstractServer object associated with the given key. # # Precondition: this method must be called within a #synchronize block. def has_key?(key) return @collection.has_key?(key) end # Checks whether the collection is empty. # # Precondition: this method must be called within a #synchronize block. def empty? return @collection.empty? end # Deletes from the collection the AbstractServer that's associated with the # given key. If no such AbstractServer exists, nothing will happen. # # If the AbstractServer is started, then it will be stopped before deletion. # # Precondition: this method must be called within a #synchronize block. def delete(key) raise ArgumentError, "cleanup() has already been called." if @done server = @collection[key] if server if server.started? server.stop end @collection.delete(key) if server.next_cleaning_time == @next_cleaning_time @next_cleaning_time = nil end end end # Notify this AbstractServerCollection that +server+ has performed an activity. # This AbstractServerCollection will update the idle information associated with +server+ # accordingly. # # lookup_or_add already automatically updates idle information, so you only need to # call this method if the time at which the server has performed an activity is # not close to the time at which lookup_or_add had been called. # # Precondition: this method must be called within a #synchronize block. def register_activity(server) if eligable_for_cleanup?(server) if server.next_cleaning_time == @next_cleaning_time @next_cleaning_time = nil end server.next_cleaning_time = Time.now + server.max_idle_time end end # Tell the cleaner thread to check the collection as soon as possible, instead # of sleeping until the next scheduled cleaning time. # # Precondition: this method must NOT be called within a #synchronize block. def check_idle_servers! @lock.synchronize do @next_cleaning_time = Time.now - 60 * 60 @cond.signal end end # Iterate over all AbstractServer objects. # # Precondition: this method must be called within a #synchronize block. def each each_pair do |key, server| yield server end end # Iterate over all keys and associated AbstractServer objects. # # Precondition: this method must be called within a #synchronize block. def each_pair raise ArgumentError, "cleanup() has already been called." if @done @collection.each_pair do |key, server| yield(key, server) end end # Delete all AbstractServers from the collection. Each AbstractServer will be # stopped, if necessary. # # Precondition: this method must be called within a #synchronize block. def clear @collection.each_value do |server| if server.started? server.stop end end @collection.clear @next_cleaning_time = nil end # Cleanup all resources used by this AbstractServerCollection. All AbstractServers # from the collection will be deleted. Each AbstractServer will be stopped, if # necessary. The background thread which removes idle AbstractServers will be stopped. # # After calling this method, this AbstractServerCollection object will become # unusable. # # Precondition: this method must *NOT* be called within a #synchronize block. def cleanup @cleanup_lock.synchronize do return if @done @lock.synchronize do @done = true @cond.signal end @cleaner_thread.join clear end end private def cleaner_thread_main while !@done current_time = Time.now # We add a 0.2 seconds delay to the sleep time because system # timers are not entirely accurate. sleep_time = (@next_cleaning_time - current_time).to_f + 0.2 if sleep_time > 0 && @cond.timed_wait(@lock, sleep_time) next else keys_to_delete = nil @next_cleaning_time = nil @collection.each_pair do |key, server| if eligable_for_cleanup?(server) # Cleanup this server if its idle timeout has expired. if server.next_cleaning_time <= current_time keys_to_delete ||= [] keys_to_delete << key if server.started? server.stop end # If not, then calculate the next cleaning time because # we're iterating the collection anyway. elsif @next_cleaning_time.nil? || server.next_cleaning_time < @next_cleaning_time @next_cleaning_time = server.next_cleaning_time end end end if keys_to_delete keys_to_delete.each do |key| @collection.delete(key) end end if @next_cleaning_time.nil? # There are no servers in the collection with an idle timeout. @next_cleaning_time = Time.now + 60 * 60 end end end end # Checks whether the given server is eligible for being idle cleaned. def eligable_for_cleanup?(server) return server.max_idle_time && server.max_idle_time != 0 end end end # module PhusionPassenger