-
Notifications
You must be signed in to change notification settings - Fork 1
/
mst_cache.lua
170 lines (151 loc) · 4.05 KB
/
mst_cache.lua
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
#!/usr/bin/env lua
-- -*-lua-*-
--
-- $Id: mst_cache.lua $
--
-- Author: Markus Stenberg <markus [email protected]>
--
-- Copyright (c) 2013 cisco Systems, Inc.
--
-- Created: Wed May 8 15:54:21 2013 mstenber
-- Last modified: Wed May 8 16:52:39 2013 mstenber
-- Edit time: 36 min
--
local mst = require 'mst'
local math = require 'math'
local ipairs = ipairs
local pairs = pairs
module(...)
--- cache class (with custom optional lifetime for replies,
--- external time source, and optional maximum number of entries)
cache = mst.create_class{class='cache',
mandatory={'get_callback'},
optional={'positive_timeout',
'negative_timeout',
'default_timeout',
'max_items',
'time_callback'}
}
function cache:init()
self:clear()
end
function cache:clear()
self.has_timeouts = self.positive_timeout or self.negative_timeout or self.default_timeout
self.map = mst.map:new{}
self.items = 0
self.op = 0
if not self.time_callback
then
-- generation based timeouts are nonsense
self:a(not self.has_timeouts)
-- default time callback is actually call counter
self.time_callback = function ()
self.op = self.op + 1
return self.op
end
end
end
function cache:get(k)
self:a(k ~= nil, 'no key')
local v = self.map[k]
if not v
then
return self:create(k)
end
-- 'v' is array, with two entries; validity and entry itself
local valid = v[1]
local value = v[2]
self:d('get', now, valid, k, value)
if self.has_timeouts
then
local now = self.time_callback()
if now > valid
then
return self:create(k)
else
return value
end
else
-- always valid
return value
end
end
function cache:create(k)
local v, t = self.get_callback(k)
self:set(k, v, t)
return v
end
function cache:purge()
local desired = math.floor(self.max_items / 2)
if self.has_timeouts
then
-- 'Cheap' variant: Check for expired entries, if they consist of
-- 'enough' things, save sorting
local now = self.time_callback()
local nm = mst.map:new{}
local cnt = mst.table_count(self.map)
self:a(cnt == self.items, 'table count mismatch', cnt, self.items)
for k, v in pairs(self.map)
do
local valid = v[1]
if now > valid
then
self.map[k] = nil
self.items = self.items - 1
end
end
if self.items <= desired
then
return
end
end
-- Cost: O(n log n) - sorting re-insertion also costs O(n) even if
-- hash is efficient but we do this on average only every n/2 times
-- => real cost is just O(log n) which is acceptable (and closer to
-- O(1) if we have just sensible number of entries in cache)
local a = mst.table_map(self.map, function (k, v)
return {v[1], v[2], k}
end)
a:sort(function (v1, v2)
local t1 = v1[1]
local t2 = v2[1]
if not t1
then
return false
end
if not t2
then
return true
end
return t1 < t2
end)
self:a(#a == self.items, 'weird a', a, #a, self.items)
self.map = mst.map:new{}
for i=#a-desired+1,#a
do
local v = a[i]
--self:a(v, 'missing index', i, #a)
self.map[v[3]] = {v[1], v[2]}
end
self.items = mst.table_count(self.map)
end
function cache:set(k, v, t)
if self.items == self.max_items
then
self:purge()
end
t = t or (v and self.positive_timeout) or self.negative_timeout or self.default_timeout
local now = self.time_callback()
if t
then
t = t + now
else
t = now
end
if self.map[k]
then
self.items = self.items - 1
end
self.map[k] = {t, v}
self.items = self.items + 1
end