yahns.git  about / heads / tags
sleepy, multi-threaded, non-blocking application server for Ruby
blob d83898ba0ae75cef070d2e6dc68feb9c593b797f 2730 bytes (raw)
$ git show v0.0.1:lib/yahns/fdmap.rb	# shows this blob on the CLI

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
 
# 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_threshold < 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

  # 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

git clone git://yhbt.net/yahns.git
git clone https://yhbt.net/yahns.git