class PhusionPassenger::AbstractServerCollection

This class is thread-safe as long as the specified thread-safety rules are followed.
collection of Railz::ApplicationSpawner objects.
and Railz::ApplicationSpawner objects, while Railz::FrameworkSpawner maintains a
of functionality. SpawnManager maintains a collection of Railz::FrameworkSpawner
This class exists because both SpawnManager and Railz::FrameworkSpawner need this kind
AbstractServers that have been idle for too long.
AbstractServerCollection also automatically takes care of cleaning up
AbstractServer objects, or look up existing ones via a key.
This class maintains a collection of AbstractServer objects. One can add new

def check_idle_servers!

Precondition: this method must NOT be called within a #synchronize block.

of sleeping until the next scheduled cleaning time.
Tell the cleaner thread to check the collection as soon as possible, instead
def check_idle_servers!
	@lock.synchronize do
		@next_cleaning_time = Time.now - 60 * 60
		@cond.signal
	end
end

def cleaner_thread_main

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

def cleanup

Precondition: this method must *NOT* be called within a #synchronize block.

unusable.
After calling this method, this AbstractServerCollection object will become

necessary. The background thread which removes idle AbstractServers will be stopped.
from the collection will be deleted. Each AbstractServer will be stopped, if
Cleanup all resources used by this AbstractServerCollection. All AbstractServers
def cleanup
	@cleanup_lock.synchronize do
		return if @done
		@lock.synchronize do
			@done = true
			@cond.signal
		end
		@cleaner_thread.join
		clear
	end
end

def clear

Precondition: this method must be called within a #synchronize block.

stopped, if necessary.
Delete all AbstractServers from the collection. Each AbstractServer will be
def clear
	@collection.each_value do |server|
		if server.started?
			server.stop
		end
	end
	@collection.clear
	@next_cleaning_time = nil
end

def delete(key)

Precondition: this method must be called within a #synchronize block.

If the AbstractServer is started, then it will be stopped before deletion.

given key. If no such AbstractServer exists, nothing will happen.
Deletes from the collection the AbstractServer that's associated with the
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

def each

Precondition: this method must be called within a #synchronize block.

Iterate over all AbstractServer objects.
def each
	each_pair do |key, server|
		yield server
	end
end

def each_pair

Precondition: this method must be called within a #synchronize block.

Iterate over all keys and associated AbstractServer objects.
def each_pair
	raise ArgumentError, "cleanup() has already been called." if @done
	@collection.each_pair do |key, server|
		yield(key, server)
	end
end

def eligable_for_cleanup?(server)

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

def empty?

Precondition: this method must be called within a #synchronize block.

Checks whether the collection is empty.
def empty?
	return @collection.empty?
end

def has_key?(key)

Precondition: this method must be called within a #synchronize block.

Checks whether there's an AbstractServer object associated with the given key.
def has_key?(key)
	return @collection.has_key?(key)
end

def initialize

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

def lookup_or_add(key)

Precondition: this method must be called within a #synchronize block.

and the exception will be propagated.
If the block raises an exception, then the collection will not be modified,

A max_idle_time value of nil or 0 means the AbstractServer will never be idle cleaned.
effect on the idle cleaning interval.
with this. Changing the value outside this block is not guaranteed to have any
AbstractServerCollection's idle cleaning interval will be adapted to accomodate
The block must set the 'max_idle_time' attribute on the AbstractServer.

that object will be stored in the collection, and returned.
block will be called. That block must return an AbstractServer object. Then,
If there is no AbstractSerer associated with the given key, then the given

Lookup and returns an AbstractServer with the given key.
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

def register_activity(server)

Precondition: this method must be called within a #synchronize block.

not close to the time at which lookup_or_add had been called.
call this method if the time at which the server has performed an activity is
lookup_or_add already automatically updates idle information, so you only need to

accordingly.
This AbstractServerCollection will update the idle information associated with +server+
Notify this AbstractServerCollection that +server+ has performed an activity.
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

def synchronize

operation.
the code within the block. The entire block will be a single atomic
Acquire the lock for this AbstractServerCollection object, and run
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