syncache 1.0.0 → 1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,283 @@
1
+ # SynCache: thread-safe time-limited cache with flexible replacement policy
2
+ # (originally written for Samizdat project)
3
+ #
4
+ # Copyright (c) 2002-2011 Dmitry Borodaenko <angdraug@debian.org>
5
+ #
6
+ # This program is free software.
7
+ # You can distribute/modify this program under the terms of
8
+ # the GNU General Public License version 3 or later.
9
+ #
10
+ # vim: et sw=2 sts=2 ts=8 tw=0
11
+
12
+ module SynCache
13
+
14
+ FOREVER = 60 * 60 * 24 * 365 * 5 # 5 years
15
+
16
+ class CacheError < RuntimeError; end
17
+
18
+ class CacheEntry
19
+ def initialize(ttl = nil, value = nil)
20
+ @value = value
21
+ @ttl = ttl
22
+ @dirty = false
23
+ record_access
24
+
25
+ @sync = Mutex.new
26
+ end
27
+
28
+ # stores the value object
29
+ attr_accessor :value
30
+
31
+ # change this to make the entry expire sooner
32
+ attr_accessor :ttl
33
+
34
+ # use this to synchronize access to +value+
35
+ attr_reader :sync
36
+
37
+ # record the fact that the entry was accessed
38
+ #
39
+ def record_access
40
+ return if @dirty
41
+ @expires = Time.now + (@ttl or FOREVER)
42
+ end
43
+
44
+ # entries with lowest index will be replaced first
45
+ #
46
+ def replacement_index
47
+ @expires
48
+ end
49
+
50
+ # check if entry is stale
51
+ #
52
+ def stale?
53
+ @expires < Time.now
54
+ end
55
+
56
+ # mark entry as dirty and schedule it to expire at given time
57
+ #
58
+ def expire_at(time)
59
+ @expires = time if @expires > time
60
+ @dirty = true
61
+ end
62
+ end
63
+
64
+ class Cache
65
+
66
+ # a float number of seconds to sleep when a race condition is detected
67
+ # (actual delay is randomized to avoid live lock situation)
68
+ #
69
+ LOCK_SLEEP = 0.2
70
+
71
+ # _ttl_ (time to live) is time in seconds from the last access until cache
72
+ # entry is expired (set to _nil_ to disable time limit)
73
+ #
74
+ # _max_size_ is max number of objects in cache
75
+ #
76
+ # _flush_delay_ is used to rate-limit flush operations: if less than that
77
+ # number of seconds has passed since last flush, next flush will be delayed;
78
+ # default is no rate limit
79
+ #
80
+ def initialize(ttl = 60*60, max_size = 5000, flush_delay = nil)
81
+ @ttl = ttl
82
+ @max_size = max_size
83
+ @debug = false
84
+
85
+ if @flush_delay = flush_delay
86
+ @last_flush = Time.now
87
+ end
88
+
89
+ @sync = Mutex.new
90
+ @cache = {}
91
+ end
92
+
93
+ # set to _true_ to report every single cache operation
94
+ #
95
+ attr_accessor :debug
96
+
97
+ # remove all values from cache
98
+ #
99
+ # if _base_ is given, only values with keys matching the base (using
100
+ # <tt>===</tt> operator) are removed
101
+ #
102
+ def flush(base = nil)
103
+ debug { 'flush ' << base.to_s }
104
+
105
+ @sync.synchronize do
106
+
107
+ if @flush_delay
108
+ next_flush = @last_flush + @flush_delay
109
+
110
+ if next_flush > Time.now
111
+ flush_at(next_flush, base)
112
+ else
113
+ flush_now(base)
114
+ @last_flush = Time.now
115
+ end
116
+
117
+ else
118
+ flush_now(base)
119
+ end
120
+ end
121
+ end
122
+
123
+ # remove single value from cache
124
+ #
125
+ def delete(key)
126
+ debug { 'delete ' << key.to_s }
127
+
128
+ @sync.synchronize do
129
+ @cache.delete(key)
130
+ end
131
+ end
132
+
133
+ # store new value in cache
134
+ #
135
+ # see also Cache#fetch_or_add
136
+ #
137
+ def []=(key, value)
138
+ debug { '[]= ' << key.to_s }
139
+
140
+ entry = get_locked_entry(key)
141
+ begin
142
+ return entry.value = value
143
+ ensure
144
+ entry.sync.unlock
145
+ end
146
+ end
147
+
148
+ # retrieve value from cache if it's still fresh
149
+ #
150
+ # see also Cache#fetch_or_add
151
+ #
152
+ def [](key)
153
+ debug { '[] ' << key.to_s }
154
+
155
+ entry = get_locked_entry(key, false)
156
+ unless entry.nil?
157
+ begin
158
+ return entry.value
159
+ ensure
160
+ entry.sync.unlock
161
+ end
162
+ end
163
+ end
164
+
165
+ # initialize missing cache entry from supplied block
166
+ #
167
+ # this is the preferred method of adding values to the cache as it locks the
168
+ # key for the duration of computation of the supplied block to prevent
169
+ # parallel execution of resource-intensive actions
170
+ #
171
+ def fetch_or_add(key)
172
+ debug { 'fetch_or_add ' << key.to_s }
173
+
174
+ entry = get_locked_entry(key)
175
+ begin
176
+ if entry.value.nil?
177
+ entry.value = yield
178
+ end
179
+ return entry.value
180
+ ensure
181
+ entry.sync.unlock
182
+ end
183
+ end
184
+
185
+ private
186
+
187
+ # immediate flush (delete all entries matching _base_)
188
+ #
189
+ # must be run from inside global lock, see #flush
190
+ #
191
+ def flush_now(base = nil)
192
+ if base
193
+ @cache.delete_if {|key, entry| base === key }
194
+ else
195
+ @cache = {}
196
+ end
197
+ end
198
+
199
+ # delayed flush (ensure all entries matching _base_ expire no later than _next_flush_)
200
+ #
201
+ # must be run from inside global lock, see #flush
202
+ #
203
+ def flush_at(next_flush, base = nil)
204
+ @cache.each do |key, entry|
205
+ next if base and not base === key
206
+ entry.expire_at(next_flush)
207
+ end
208
+ end
209
+
210
+ def add_blank_entry(key)
211
+ @sync.locked? or raise CacheError,
212
+ 'add_entry called while @sync is not locked'
213
+
214
+ had_same_key = @cache.has_key?(key)
215
+ entry = @cache[key] = CacheEntry.new(@ttl)
216
+ check_size unless had_same_key
217
+ entry
218
+ end
219
+
220
+ def get_locked_entry(key, add_if_missing=true)
221
+ debug { "get_locked_entry #{key}, #{add_if_missing}" }
222
+
223
+ entry = nil # scope fix
224
+ entry_locked = false
225
+ until entry_locked do
226
+ @sync.synchronize do
227
+ entry = @cache[key]
228
+
229
+ if entry.nil? or entry.stale?
230
+ if add_if_missing
231
+ entry = add_blank_entry(key)
232
+ else
233
+ @cache.delete(key) unless entry.nil?
234
+ return nil
235
+ end
236
+ end
237
+
238
+ entry_locked = entry.sync.try_lock
239
+ end
240
+ sleep(rand * LOCK_SLEEP) unless entry_locked
241
+ end
242
+
243
+ entry.record_access
244
+ entry
245
+ end
246
+
247
+ # remove oldest item from cache if size limit reached
248
+ #
249
+ def check_size
250
+ debug { 'check_size' }
251
+
252
+ return unless @max_size.kind_of? Numeric
253
+
254
+ if @sync.locked?
255
+ check_size_internal
256
+ else
257
+ @sync.synchronize { check_size_internal }
258
+ end
259
+ end
260
+
261
+ def check_size_internal
262
+ while @cache.size > @max_size do
263
+ # optimize: supplement hash with queue
264
+ oldest = @cache.keys.min {|a, b| @cache[a].replacement_index <=> @cache[b].replacement_index }
265
+ @cache.delete(oldest)
266
+ end
267
+ end
268
+
269
+ # send debug output to syslog if enabled
270
+ #
271
+ def debug
272
+ return unless @debug
273
+ message = Thread.current.to_s + ' ' + yield
274
+ if defined?(Syslog) and Syslog.opened?
275
+ Syslog.debug(message)
276
+ else
277
+ STDERR << 'syncache: ' + message + "\n"
278
+ STDERR.flush
279
+ end
280
+ end
281
+ end
282
+
283
+ end # module SynCache
data/lib/syncache.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # SynCache: thread-safe time-limited cache with flexible replacement policy
2
2
  # (originally written for Samizdat project)
3
3
  #
4
- # Copyright (c) 2002-2009 Dmitry Borodaenko <angdraug@debian.org>
4
+ # Copyright (c) 2002-2011 Dmitry Borodaenko <angdraug@debian.org>
5
5
  #
6
6
  # This program is free software.
7
7
  # You can distribute/modify this program under the terms of
@@ -9,250 +9,5 @@
9
9
  #
10
10
  # vim: et sw=2 sts=2 ts=8 tw=0
11
11
 
12
- require 'sync'
13
- require 'syncache_sync_patch'
14
-
15
- module SynCache
16
-
17
- FOREVER = 60 * 60 * 24 * 365 * 5 # 5 years
18
-
19
- class CacheEntry
20
- def initialize(ttl = nil, value = nil)
21
- @value = value
22
- @ttl = ttl
23
- @dirty = false
24
- record_access
25
-
26
- @sync = Sync.new
27
- end
28
-
29
- # stores the value object
30
- attr_accessor :value
31
-
32
- # change this to make the entry expire sooner
33
- attr_accessor :ttl
34
-
35
- # use this to synchronize access to +value+
36
- attr_reader :sync
37
-
38
- # record the fact that the entry was accessed
39
- #
40
- def record_access
41
- return if @dirty
42
- @expires = Time.now + (@ttl or FOREVER)
43
- end
44
-
45
- # entries with lowest index will be replaced first
46
- #
47
- def replacement_index
48
- @expires
49
- end
50
-
51
- # check if entry is stale
52
- #
53
- def stale?
54
- @expires < Time.now
55
- end
56
-
57
- # mark entry as dirty and schedule it to expire at given time
58
- #
59
- def expire_at(time)
60
- @expires = time if @expires > time
61
- @dirty = true
62
- end
63
- end
64
-
65
- class Cache
66
-
67
- # set to _true_ to report every single cache operation to syslog
68
- #
69
- DEBUG = false
70
-
71
- # a float number of seconds to sleep when a race condition is detected
72
- # (actual delay is randomized to avoid live lock situation)
73
- #
74
- LOCK_SLEEP = 0.2
75
-
76
- # _ttl_ (time to live) is time in seconds from the last access until cache
77
- # entry is expired (set to _nil_ to disable time limit)
78
- #
79
- # _max_size_ is max number of objects in cache
80
- #
81
- # _flush_delay_ is used to rate-limit flush operations: if less than that
82
- # number of seconds has passed since last flush, next flush will be delayed;
83
- # default is no rate limit
84
- #
85
- def initialize(ttl = 60*60, max_size = 5000, flush_delay = nil)
86
- @ttl = ttl
87
- @max_size = max_size
88
-
89
- if @flush_delay = flush_delay
90
- @last_flush = Time.now
91
- end
92
-
93
- @sync = Sync.new
94
- @cache = {}
95
- end
96
-
97
- # remove all values from cache
98
- #
99
- # if _base_ is given, only values with keys matching the base (using
100
- # <tt>===</tt> operator) are removed
101
- #
102
- def flush(base = nil)
103
- debug('flush ' << base.to_s)
104
-
105
- @sync.synchronize do
106
-
107
- if @flush_delay
108
- next_flush = @last_flush + @flush_delay
109
-
110
- if next_flush > Time.now
111
- flush_at(next_flush, base)
112
- else
113
- flush_now(base)
114
- @last_flush = Time.now
115
- end
116
-
117
- else
118
- flush_now(base)
119
- end
120
- end
121
- end
122
-
123
- # remove single value from cache
124
- #
125
- def delete(key)
126
- debug('delete ' << key.to_s)
127
-
128
- @sync.synchronize do
129
- @cache.delete(key)
130
- end
131
- end
132
-
133
- # store new value in cache
134
- #
135
- # see also Cache#fetch_or_add
136
- #
137
- def []=(key, value)
138
- debug('[]= ' << key.to_s)
139
-
140
- entry = get_entry(key)
141
- entry.sync.synchronize do
142
- entry.value = value
143
- end
144
- value
145
- end
146
-
147
- # retrieve value from cache if it's still fresh
148
- #
149
- # see also Cache#fetch_or_add
150
- #
151
- def [](key)
152
- debug('[] ' << key.to_s)
153
-
154
- entry = get_entry(key)
155
- entry.sync.synchronize(:SH) do
156
- entry.value
157
- end
158
- end
159
-
160
- # initialize missing cache entry from supplied block
161
- #
162
- # this is the preferred method of adding values to the cache as it locks the
163
- # key for the duration of computation of the supplied block to prevent
164
- # parallel execution of resource-intensive actions
165
- #
166
- def fetch_or_add(key)
167
- debug('fetch_or_add ' << key.to_s)
168
-
169
- entry = nil # scope fix
170
- entry_locked = false
171
- until entry_locked do
172
- @sync.synchronize do
173
- entry = get_entry(key)
174
- entry_locked = entry.sync.try_lock # fixme
175
- end
176
- sleep(rand * LOCK_SLEEP) unless entry_locked
177
- end
178
-
179
- begin
180
- entry.record_access
181
- entry.value ||= yield
182
- ensure
183
- entry.sync.unlock
184
- end
185
- end
186
-
187
- private
188
-
189
- # immediate flush (delete all entries matching _base_)
190
- #
191
- # must be run from inside global lock, see #flush
192
- #
193
- def flush_now(base = nil)
194
- if base
195
- @cache.delete_if {|key, entry| base === key }
196
- else
197
- @cache = {}
198
- end
199
- end
200
-
201
- # delayed flush (ensure all entries matching _base_ expire no later than _next_flush_)
202
- #
203
- # must be run from inside global lock, see #flush
204
- #
205
- def flush_at(next_flush, base = nil)
206
- @cache.each do |key, entry|
207
- next if base and not base === key
208
- entry.expire_at(next_flush)
209
- end
210
- end
211
-
212
- def get_entry(key)
213
- debug('get_entry ' << key.to_s)
214
-
215
- @sync.synchronize do
216
- entry = @cache[key]
217
-
218
- if entry.kind_of?(CacheEntry)
219
- if entry.stale?
220
- @cache[key] = entry = CacheEntry.new(@ttl)
221
- end
222
- else
223
- @cache[key] = entry = CacheEntry.new(@ttl)
224
- check_size
225
- end
226
-
227
- entry.record_access
228
- entry
229
- end
230
- end
231
-
232
- # remove oldest item from cache if size limit reached
233
- #
234
- def check_size
235
- debug('check_size')
236
-
237
- return unless @max_size.kind_of? Numeric
238
-
239
- @sync.synchronize do
240
- while @cache.size > @max_size do
241
- # optimize: supplement hash with queue
242
- oldest = @cache.keys.min {|a, b| @cache[a].replacement_index <=> @cache[b].replacement_index }
243
-
244
- @cache.delete(oldest)
245
- end
246
- end
247
- end
248
-
249
- # send debug output to syslog if enabled
250
- #
251
- def debug(message)
252
- if DEBUG and defined?(Syslog) and Syslog.opened?
253
- Syslog.debug(Thread.current.to_s << ' ' << message)
254
- end
255
- end
256
- end
257
-
258
- end # module SynCache
12
+ require 'syncache/syncache'
13
+ require 'syncache/remote'
@@ -0,0 +1,44 @@
1
+ .TH "SYNCACHE-DRB" "1"
2
+ .SH "NAME"
3
+ syncache-drb - SynCache dRuby object cache server
4
+ .SH "SYNOPSIS"
5
+ .PP
6
+ \fBsyncache-drb\fP [ \fBoptions\fP ] [ \fBURI\fP ]
7
+ .SH "DESCRIPTION"
8
+ .PP
9
+ \fBsyncache-drb\fP starts a Distributed Ruby server providing a
10
+ SynCache::Cache object.
11
+ .PP
12
+ SynCache::Cache is a thread-safe time-limited object cache with flexible
13
+ replacement strategy.
14
+ .SH "OPTIONS"
15
+ .IP "\fBURI\fP" 4
16
+ A URI with druby: schema that the DRb server binds to, default is
17
+ \fBdruby://localhost:9000\fP
18
+ .IP "\fB--help\fP" 4
19
+ Display usage information and quit.
20
+ .IP "\fB--ttl\fP SECONDS" 4
21
+ Time-to-live value for cache entries, default is 24 hours.
22
+ .IP "\fB--size\fP ENTRIES" 4
23
+ Maximum number of objects in cache, default is 10000.
24
+ .IP "\fB--flush-delay\fP SECONDS" 4
25
+ Rate-limit flush operations. If less than that number of seconds has passed
26
+ since last flush, next flush will be delayed. Default is no rate limit.
27
+ .IP "\fB--user\fP USER" 4
28
+ Run as USER if started as root. Default is nobody.
29
+ .IP "\fB--error-log\fP ERROR_LOG_PATH" 4
30
+ File to write errors to. Default is /dev/null. When run as root,
31
+ the file is chowned to USER:adm.
32
+ .IP "\fB--debug\fP" 4
33
+ Enable debug mode. If an error log is specified with --error-log, all
34
+ messages will be sent there instead of syslog.
35
+ .IP "\fB--pidfile\fP PATH" 4
36
+ Path to pidfile. By default, pidfile is created under /var/run/syncache-drb/
37
+ when run as root, or under $TMPDIR otherwise. Location should be writeable by
38
+ USER.
39
+
40
+ .SH "AUTHOR"
41
+ .PP
42
+ This manual page was written by Dmitry Borodaenko <angdraug@debian.org>.
43
+ Permission is granted to copy, distribute and/or modify this document
44
+ under the terms of the GNU GPL version 3 or later.