about summary refs log tree commit homepage
path: root/lib/yahns/fdmap.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/yahns/fdmap.rb')
-rw-r--r--lib/yahns/fdmap.rb90
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