# Phusion Passenger - http://www.modrails.com/# Copyright (c) 2010 Phusion## "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui.## Permission is hereby granted, free of charge, to any person obtaining a copy# of this software and associated documentation files (the "Software"), to deal# in the Software without restriction, including without limitation the rights# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell# copies of the Software, and to permit persons to whom the Software is# furnished to do so, subject to the following conditions:## The above copyright notice and this permission notice shall be included in# all copies or substantial portions of the Software.## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN# THE SOFTWARE.require'phusion_passenger/utils'modulePhusionPassenger# 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 ClassicRails::FrameworkSpawner need this kind# of functionality. SpawnManager maintains a collection of ClassicRails::FrameworkSpawner# and ClassicRails::ApplicationSpawner objects, while ClassicRails::FrameworkSpawner maintains a# collection of ClassicRails::ApplicationSpawner objects.## This class is thread-safe as long as the specified thread-safety rules are followed.classAbstractServerCollectionattr_reader:next_cleaning_timeincludeUtilsdefinitialize@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.newdobegin@lock.synchronizedocleaner_thread_mainendrescueException=>eprint_exception(self.class.to_s,e)endendend# Acquire the lock for this AbstractServerCollection object, and run# the code within the block. The entire block will be a single atomic# operation.defsynchronize@lock.synchronizedo@in_synchronize_block=truebeginyieldensureif@next_cleaning_time.nil?@collection.each_valuedo|server|if@next_cleaning_time.nil?||(eligable_for_cleanup?(server)&&server.next_cleaning_time<@next_cleaning_time)@next_cleaning_time=server.next_cleaning_timeendendif@next_cleaning_time.nil?# There are no servers in the collection with an idle timeout.@next_cleaning_time=Time.now+60*60end@next_cleaning_time_changed=trueendif@next_cleaning_time_changed@next_cleaning_time_changed=false@cond.signalend@in_synchronize_block=falseendendend# 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.deflookup_or_add(key)raiseArgumentError,"cleanup() has already been called."if@donemust_be_in_synchronize_blockserver=@collection[key]ifserverregister_activity(server)returnserverelseserver=yieldif!server.respond_to?(:start)raiseTypeError,"The block didn't return a valid AbstractServer object."endifeligable_for_cleanup?(server)server.next_cleaning_time=Time.now+server.max_idle_timeif@next_cleaning_time&&server.next_cleaning_time<@next_cleaning_time@next_cleaning_time=server.next_cleaning_time@next_cleaning_time_changed=trueendend@collection[key]=serverreturnserverendend# Checks whether there's an AbstractServer object associated with the given key.## Precondition: this method must be called within a #synchronize block.defhas_key?(key)must_be_in_synchronize_blockreturn@collection.has_key?(key)end# Checks whether the collection is empty.## Precondition: this method must be called within a #synchronize block.defempty?must_be_in_synchronize_blockreturn@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.defdelete(key)raiseArgumentError,"cleanup() has already been called."if@donemust_be_in_synchronize_blockserver=@collection[key]ifserverifserver.started?server.stopend@collection.delete(key)ifserver.next_cleaning_time==@next_cleaning_time@next_cleaning_time=nilendendend# 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.defregister_activity(server)must_be_in_synchronize_blockifeligable_for_cleanup?(server)ifserver.next_cleaning_time==@next_cleaning_time@next_cleaning_time=nilendserver.next_cleaning_time=Time.now+server.max_idle_timeendend# 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.defcheck_idle_servers!must_not_be_in_synchronize_block@lock.synchronizedo@next_cleaning_time=Time.now-60*60@cond.signalendend# Iterate over all AbstractServer objects.## Precondition: this method must be called within a #synchronize block.defeachmust_be_in_synchronize_blockeach_pairdo|key,server|yieldserverendend# Iterate over all keys and associated AbstractServer objects.## Precondition: this method must be called within a #synchronize block.defeach_pairraiseArgumentError,"cleanup() has already been called."if@donemust_be_in_synchronize_block@collection.each_pairdo|key,server|yield(key,server)endend# Delete all AbstractServers from the collection. Each AbstractServer will be# stopped, if necessary.## Precondition: this method must be called within a #synchronize block.defclearmust_be_in_synchronize_block@collection.each_valuedo|server|ifserver.started?server.stopendend@collection.clear@next_cleaning_time=nilend# 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.defcleanupmust_not_be_in_synchronize_block@cleanup_lock.synchronizedoreturnif@done@lock.synchronizedo@done=true@cond.signalend@cleaner_thread.joinsynchronizedoclearendendendprivatedefcleaner_thread_mainwhile!@donecurrent_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.2ifsleep_time>0&&@cond.timed_wait(@lock,sleep_time)nextelsekeys_to_delete=nil@next_cleaning_time=nil@collection.each_pairdo|key,server|ifeligable_for_cleanup?(server)# Cleanup this server if its idle timeout has expired.ifserver.next_cleaning_time<=current_timekeys_to_delete||=[]keys_to_delete<<keyifserver.started?server.stopend# 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_timeendendendifkeys_to_deletekeys_to_delete.eachdo|key|@collection.delete(key)endendif@next_cleaning_time.nil?# There are no servers in the collection with an idle timeout.@next_cleaning_time=Time.now+60*60endendendend# Checks whether the given server is eligible for being idle cleaned.defeligable_for_cleanup?(server)returnserver.max_idle_time&&server.max_idle_time!=0enddefmust_be_in_synchronize_blockif!@in_synchronize_blockraiseRuntimeError,"This method may only be called within a #synchronize block!"endenddefmust_not_be_in_synchronize_blockif@in_synchronize_blockraiseRuntimeError,"This method may NOT be called within a #synchronize block!"endendendend# module PhusionPassenger