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
|