diff options
Diffstat (limited to 'lib/yahns/fdmap.rb')
-rw-r--r-- | lib/yahns/fdmap.rb | 90 |
1 files changed, 90 insertions, 0 deletions
diff --git a/lib/yahns/fdmap.rb b/lib/yahns/fdmap.rb new file mode 100644 index 0000000..0272421 --- /dev/null +++ b/lib/yahns/fdmap.rb @@ -0,0 +1,90 @@ +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require 'thread' + +# only initialize this after forking, this is highly volatile and won't +# be able to share data across processes at all. +# This is really a singleton + +class Yahns::Fdmap # :nodoc: + def initialize(logger, client_expire_threshold) + @logger = logger + + if Float === client_expire_threshold + client_expire_threshold *= Process.getrlimit(:NOFILE)[0] + elsif client_expire_treshhold < 0 + client_expire_threshold = Process.getrlimit(:NOFILE)[0] - + client_expire_threshold + end + @client_expire_threshold = client_expire_threshold.to_i + + # This is an array because any sane OS will frequently reuse FDs + # to keep this tightly-packed and favor lower FD numbers + # (consider select(2) performance (not that we use select)) + # An (unpacked) Hash (in MRI) uses 5 more words per entry than an Array, + # and we should expect this array to have around 60K elements + @fdmap_ary = [] + @fdmap_mtx = Mutex.new + @last_expire = 0.0 + @count = 0 + end + + # called immediately after accept() + def add(io) + fd = io.fileno + @fdmap_mtx.synchronize do + if (@count += 1) > @client_expire_threshold + __expire_for(io) + else + @fdmap_ary[fd] = io + end + end + end + + # this is only called in Errno::EMFILE/Errno::ENFILE situations + def desperate_expire_for(io, timeout) + @fdmap_mtx.synchronize { __expire_for(io, timeout) } + end + + # called before IO#close + def decr + # don't bother clearing the element in @fdmap_ary, it'll just be + # overwritten when another client connects (soon). We must not touch + # @fdmap_ary[io.fileno] after IO#close on io + @fdmap_mtx.synchronize { @count -= 1 } + end + + def delete(io) # use with rack.hijack (via yahns) + fd = io.fileno + @fdmap_mtx.synchronize do + @fdmap_ary[fd] = nil + @count -= 1 + end + end + + # expire a bunch of idle clients and register the current one + # We should not be calling this too frequently, it is expensive + # This is called while @fdmap_mtx is held + def __expire_for(io, timeout = nil) + nr = 0 + now = Time.now.to_f + (now - @last_expire) >= 1.0 or return # don't expire too frequently + + # @fdmap_ary may be huge, so always expire a bunch at once to + # avoid getting to this method too frequently + @fdmap_ary.each do |c| + c.respond_to?(:yahns_expire) or next + nr += c.yahns_expire(timeout || c.class.client_timeout) + end + + @fdmap_ary[io.fileno] = io + @last_expire = Time.now.to_f + msg = timeout ? "timeout=#{timeout})" : "client_timeout" + @logger.info("dropping #{nr} of #@count clients for #{msg}") + end + + # used for graceful shutdown + def size + @fdmap_mtx.synchronize { @count } + end +end |