class PhusionPassenger::AbstractServerCollection

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.

Attributes

next_cleaning_time[R]

Public Class Methods

new() click to toggle source
# File lib/phusion_passenger/abstract_server_collection.rb, line 44
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

Public Instance Methods

check_idle_servers!() click to toggle source

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.

# File lib/phusion_passenger/abstract_server_collection.rb, line 208
def check_idle_servers!
        must_not_be_in_synchronize_block
        @lock.synchronize do
                @next_cleaning_time = Time.now - 60 * 60
                @cond.signal
        end
end
cleanup() click to toggle source

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.

# File lib/phusion_passenger/abstract_server_collection.rb, line 260
def cleanup
        must_not_be_in_synchronize_block
        @cleanup_lock.synchronize do
                return if @done
                @lock.synchronize do
                        @done = true
                        @cond.signal
                end
                @cleaner_thread.join
                synchronize do
                        clear
                end
        end
end
clear() click to toggle source

Delete all AbstractServers from the collection. Each AbstractServer will be stopped, if necessary.

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

# File lib/phusion_passenger/abstract_server_collection.rb, line 241
def clear
        must_be_in_synchronize_block
        @collection.each_value do |server|
                if server.started?
                        server.stop
                end
        end
        @collection.clear
        @next_cleaning_time = nil
end
delete(key) click to toggle source

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.

# File lib/phusion_passenger/abstract_server_collection.rb, line 170
def delete(key)
        raise ArgumentError, "cleanup() has already been called." if @done
        must_be_in_synchronize_block
        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
each() { |server| ... } click to toggle source

Iterate over all AbstractServer objects.

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

# File lib/phusion_passenger/abstract_server_collection.rb, line 219
def each
        must_be_in_synchronize_block
        each_pair do |key, server|
                yield server
        end
end
each_pair() { |key, server| ... } click to toggle source

Iterate over all keys and associated AbstractServer objects.

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

# File lib/phusion_passenger/abstract_server_collection.rb, line 229
def each_pair
        raise ArgumentError, "cleanup() has already been called." if @done
        must_be_in_synchronize_block
        @collection.each_pair do |key, server|
                yield(key, server)
        end
end
empty?() click to toggle source

Checks whether the collection is empty.

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

# File lib/phusion_passenger/abstract_server_collection.rb, line 159
def empty?
        must_be_in_synchronize_block
        return @collection.empty?
end
has_key?(key) click to toggle source

Checks whether there's an AbstractServer object associated with the given key.

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

# File lib/phusion_passenger/abstract_server_collection.rb, line 151
def has_key?(key)
        must_be_in_synchronize_block
        return @collection.has_key?(key)
end
lookup_or_add(key) { || ... } click to toggle source

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.

# File lib/phusion_passenger/abstract_server_collection.rb, line 124
def lookup_or_add(key)
        raise ArgumentError, "cleanup() has already been called." if @done
        must_be_in_synchronize_block
        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
register_activity(server) click to toggle source

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.

# File lib/phusion_passenger/abstract_server_collection.rb, line 194
def register_activity(server)
        must_be_in_synchronize_block
        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
synchronize() { || ... } click to toggle source

Acquire the lock for this AbstractServerCollection object, and run the code within the block. The entire block will be a single atomic operation.

# File lib/phusion_passenger/abstract_server_collection.rb, line 78
def synchronize
        @lock.synchronize do
                @in_synchronize_block = true
                begin
                        yield
                ensure
                        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
                        @in_synchronize_block = false
                end
        end
end

Private Instance Methods

cleaner_thread_main() click to toggle source
# File lib/phusion_passenger/abstract_server_collection.rb, line 276
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
eligable_for_cleanup?(server) click to toggle source

Checks whether the given server is eligible for being idle cleaned.

# File lib/phusion_passenger/abstract_server_collection.rb, line 318
def eligable_for_cleanup?(server)
        return server.max_idle_time && server.max_idle_time != 0
end
must_be_in_synchronize_block() click to toggle source
# File lib/phusion_passenger/abstract_server_collection.rb, line 322
def must_be_in_synchronize_block
        if !@in_synchronize_block
                raise RuntimeError, "This method may only be called within a #synchronize block!"
        end
end
must_not_be_in_synchronize_block() click to toggle source
# File lib/phusion_passenger/abstract_server_collection.rb, line 328
def must_not_be_in_synchronize_block
        if @in_synchronize_block
                raise RuntimeError, "This method may NOT be called within a #synchronize block!"
        end
end