mloughran-api_cache 0.1.2 → 0.2.0
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.
- data/README.rdoc +94 -0
- data/VERSION.yml +2 -2
- data/lib/api_cache.rb +101 -38
- data/lib/api_cache/abstract_store.rb +25 -23
- data/lib/api_cache/api.rb +85 -58
- data/lib/api_cache/cache.rb +54 -35
- data/lib/api_cache/memory_store.rb +28 -26
- data/lib/api_cache/moneta_store.rb +29 -0
- data/spec/api_cache_spec.rb +66 -37
- data/spec/api_spec.rb +60 -5
- data/spec/cache_spec.rb +36 -0
- data/spec/integration_spec.rb +73 -0
- data/spec/monteta_store_spec.rb +30 -0
- data/spec/spec_helper.rb +2 -2
- metadata +35 -13
- data/README.markdown +0 -80
- data/lib/api_cache/logger.rb +0 -9
- data/lib/api_cache/memcache_store.rb +0 -54
data/README.rdoc
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
= APICache (aka api_cache)
|
2
|
+
|
3
|
+
APICache allows any API client library to be easily wrapped with a robust caching layer. It supports caching (obviously), serving stale data and limits on the number of API calls. It's also got a handy syntax if all you want to do is cache a bothersome url.
|
4
|
+
|
5
|
+
== For the impatient
|
6
|
+
|
7
|
+
# Install
|
8
|
+
sudo gem install api_cache -s http://gemcutter.org
|
9
|
+
|
10
|
+
# Require
|
11
|
+
require 'rubygems'
|
12
|
+
require 'api_cache'
|
13
|
+
|
14
|
+
# Use
|
15
|
+
APICache.get("http://twitter.com/statuses/public_timeline.rss")
|
16
|
+
|
17
|
+
# Use a proper store
|
18
|
+
require 'moneta/memcache'
|
19
|
+
APICache.store = Moneta::Memcache.new(:server => "localhost")
|
20
|
+
|
21
|
+
# Wrap an API, and handle the failure case
|
22
|
+
|
23
|
+
APICache.get("my_albums", :fail => []) do
|
24
|
+
FlickrRb.get_all_sets
|
25
|
+
end
|
26
|
+
|
27
|
+
== The longer version
|
28
|
+
|
29
|
+
You want to use the Twitter API but you don't want to die?
|
30
|
+
|
31
|
+
APICache.get("http://twitter.com/statuses/public_timeline.rss")
|
32
|
+
|
33
|
+
This works better than a standard HTTP get because you get the following functionality for free:
|
34
|
+
|
35
|
+
* Cached response returned for 10 minutes
|
36
|
+
* Stale response returned for a day if twitter is down
|
37
|
+
* Limited to attempt a connection at most once a minute
|
38
|
+
|
39
|
+
To understand what <tt>APICache</tt> does here's an example: Given cached data less than 10 minutes old, it returns that. Otherwise, assuming it didn't try to request the URL within the last minute (to avoid the rate limit), it makes a get request to the supplied url. If the Twitter API timeouts or doesn't return a 2xx code (very likely) we're still fine: it just returns the last data fetched (as long as it's less than a day old). In the exceptional case that all is lost and no data can be returned, a subclass of <tt>APICache::APICacheError</tt> is raised which you're responsible for rescuing.
|
40
|
+
|
41
|
+
Assuming that you don't care whether it was a timeout error or an invalid response (for example) you could do this:
|
42
|
+
|
43
|
+
begin
|
44
|
+
APICache.get("http://twitter.com/statuses/public_timeline.rss")
|
45
|
+
rescue APICache::APICacheError
|
46
|
+
"Fail Whale"
|
47
|
+
end
|
48
|
+
|
49
|
+
However there's an easier way if you don't care exactly why the API call failed. You can just pass the :fail parameter (procs are accepted too) and all exceptions will be rescued for you. So this is exactly equivalent:
|
50
|
+
|
51
|
+
APICache.get("http://twitter.com/statuses/public_timeline.rss", {
|
52
|
+
:fail => "Fail Whale"
|
53
|
+
})
|
54
|
+
|
55
|
+
The real value however is not caching HTTP calls, but allowing caching functionality to be easily added to existing API client gems, or in fact any arbitrary code which is either slow or not guaranteed to succeed every time.
|
56
|
+
|
57
|
+
APICache.get('twitter_replies', :cache => 3600) do
|
58
|
+
Net::HTTP.start('twitter.com') do |http|
|
59
|
+
req = Net::HTTP::Get.new('/statuses/replies.xml')
|
60
|
+
req.basic_auth 'username', 'password'
|
61
|
+
response = http.request(req)
|
62
|
+
case response
|
63
|
+
when Net::HTTPSuccess
|
64
|
+
# 2xx response code
|
65
|
+
response.body
|
66
|
+
else
|
67
|
+
raise APICache::InvalidResponse
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
The first argument to <tt>APICache.get</tt> is now assumed to be a unique key rather than a URL. As you'd expect, the block will only be called if the request cannot be fulfilled by the cache. Throwing any exception signals to <tt>APICache</tt> that the request was not successful, should not be cached, and a cached value should be returned if available. If a cached value is not available then the exception will be re-raised for you to handle.
|
73
|
+
|
74
|
+
You can send any of the following options to <tt>APICache.get(url, options = {}, &block)</tt>. These are the default values (times are all in seconds):
|
75
|
+
|
76
|
+
{
|
77
|
+
:cache => 600, # 10 minutes After this time fetch new data
|
78
|
+
:valid => 86400, # 1 day Maximum time to use old data
|
79
|
+
# :forever is a valid option
|
80
|
+
:period => 60, # 1 minute Maximum frequency to call API
|
81
|
+
:timeout => 5 # 5 seconds API response timeout
|
82
|
+
:fail => # Value returned instead of exception on failure
|
83
|
+
}
|
84
|
+
|
85
|
+
Before using the APICache you should set the cache to use. By default an in memory hash is used - obviously not a great idea. Thankfully APICache can use any moneta store, so for example if you wanted to use memcache you'd do this:
|
86
|
+
|
87
|
+
require 'moneta/memcache'
|
88
|
+
APICache.store = Moneta::Memcache.new(:server => "localhost")
|
89
|
+
|
90
|
+
Please be liberal with the github issue tracker, more so with pull requests, or drop me a mail to me [at] mloughran [dot] com. I'd love to hear from you.
|
91
|
+
|
92
|
+
== Copyright
|
93
|
+
|
94
|
+
Copyright (c) 2008 Martyn Loughran. See LICENSE for details.
|
data/VERSION.yml
CHANGED
data/lib/api_cache.rb
CHANGED
@@ -1,64 +1,128 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
# Contains the complete public API for APICache.
|
4
|
+
#
|
5
|
+
# See APICache.get method and the README file.
|
6
|
+
#
|
7
|
+
# Before using APICache you should set the store to use using
|
8
|
+
# APICache.store=. For convenience, when no store is set an in memory store
|
9
|
+
# will be used (and a warning will be logged).
|
10
|
+
#
|
1
11
|
class APICache
|
2
|
-
class
|
3
|
-
class
|
4
|
-
class
|
5
|
-
|
12
|
+
class APICacheError < RuntimeError; end
|
13
|
+
class NotAvailableError < APICacheError; end
|
14
|
+
class TimeoutError < NotAvailableError; end
|
15
|
+
class InvalidResponse < NotAvailableError; end
|
16
|
+
class CannotFetch < NotAvailableError; end
|
17
|
+
|
6
18
|
class << self
|
7
|
-
attr_accessor :cache
|
8
|
-
attr_accessor :api
|
9
19
|
attr_accessor :logger
|
20
|
+
attr_accessor :store
|
21
|
+
|
22
|
+
def logger # :nodoc:
|
23
|
+
@logger ||= begin
|
24
|
+
log = Logger.new(STDOUT)
|
25
|
+
log.level = Logger::INFO
|
26
|
+
log
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Set the logger to use. If not set, <tt>Logger.new(STDOUT)</tt> will be
|
31
|
+
# used.
|
32
|
+
#
|
33
|
+
def logger=(logger)
|
34
|
+
@logger = logger
|
35
|
+
end
|
36
|
+
|
37
|
+
def store # :nodoc:
|
38
|
+
@store ||= begin
|
39
|
+
APICache.logger.warn("Using in memory store")
|
40
|
+
APICache::MemoryStore.new
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Set the cache store to use. This should either be an instance of a
|
45
|
+
# moneta store or a subclass of APICache::AbstractStore. Moneta is
|
46
|
+
# recommended.
|
47
|
+
#
|
48
|
+
def store=(store)
|
49
|
+
@store = begin
|
50
|
+
if store.class < APICache::AbstractStore
|
51
|
+
store
|
52
|
+
elsif store.class.to_s =~ /Moneta/
|
53
|
+
MonetaStore.new(store)
|
54
|
+
elsif store.nil?
|
55
|
+
nil
|
56
|
+
else
|
57
|
+
raise ArgumentError, "Please supply an instance of either a moneta store or a subclass of APICache::AbstractStore"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
10
61
|
end
|
11
|
-
|
12
|
-
#
|
13
|
-
def self.start(store = APICache::MemcacheStore, logger = APICache::Logger.new)
|
14
|
-
APICache.logger = logger
|
15
|
-
APICache.cache = APICache::Cache.new(store)
|
16
|
-
APICache.api = APICache::API.new
|
17
|
-
end
|
18
|
-
|
19
|
-
# Raises an APICache::NotAvailableError if it can't get a value. You should rescue this
|
20
|
-
# if your application code.
|
21
|
-
#
|
22
|
-
# Optionally call with a block. The value of the block is then used to
|
62
|
+
|
63
|
+
# Optionally call with a block. The value of the block is then used to
|
23
64
|
# set the cache rather than calling the url. Use it for example if you need
|
24
65
|
# to make another type of request, catch custom error codes etc. To signal
|
25
|
-
# that the call failed just raise
|
26
|
-
# not be cached and the api will not be called again for options[:timeout]
|
27
|
-
# seconds. If an old value is available in the cache then it will be
|
28
|
-
#
|
66
|
+
# that the call failed just raise any exception - the value will then
|
67
|
+
# not be cached and the api will not be called again for options[:timeout]
|
68
|
+
# seconds. If an old value is available in the cache then it will be
|
69
|
+
# returned.
|
70
|
+
#
|
71
|
+
# An exception will be raised if the API cannot be fetched and the request
|
72
|
+
# cannot be served by the cache. This will either be a subclass of
|
73
|
+
# APICache::Error or an exception raised by the provided block.
|
74
|
+
#
|
29
75
|
# For example:
|
30
76
|
# APICache.get("http://twitter.com/statuses/user_timeline/6869822.atom")
|
31
|
-
#
|
77
|
+
#
|
32
78
|
# APICache.get \
|
33
79
|
# "http://twitter.com/statuses/user_timeline/6869822.atom",
|
34
80
|
# :cache => 60, :valid => 600
|
81
|
+
#
|
35
82
|
def self.get(key, options = {}, &block)
|
36
83
|
options = {
|
37
84
|
:cache => 600, # 10 minutes After this time fetch new data
|
38
85
|
:valid => 86400, # 1 day Maximum time to use old data
|
39
|
-
|
86
|
+
# :forever is a valid option
|
40
87
|
:period => 60, # 1 minute Maximum frequency to call API
|
41
88
|
:timeout => 5 # 5 seconds API response timeout
|
42
89
|
}.merge(options)
|
43
|
-
|
44
|
-
|
45
|
-
|
90
|
+
|
91
|
+
cache = APICache::Cache.new(key, {
|
92
|
+
:cache => options[:cache],
|
93
|
+
:valid => options[:valid]
|
94
|
+
})
|
95
|
+
|
96
|
+
api = APICache::API.new(key, {
|
97
|
+
:period => options[:period],
|
98
|
+
:timeout => options[:timeout]
|
99
|
+
}, &block)
|
100
|
+
|
101
|
+
cache_state = cache.state
|
102
|
+
|
46
103
|
if cache_state == :current
|
47
|
-
cache.get
|
104
|
+
cache.get
|
48
105
|
else
|
49
106
|
begin
|
50
|
-
|
51
|
-
|
52
|
-
cache.set(key, value)
|
107
|
+
value = api.get
|
108
|
+
cache.set(value)
|
53
109
|
value
|
54
|
-
rescue
|
55
|
-
APICache.logger.
|
110
|
+
rescue => e
|
111
|
+
APICache.logger.info "Failed to fetch from API - Exception: " \
|
56
112
|
"#{e.class}: #{e.message}"
|
113
|
+
# No point outputting backgraces for internal APICache errors
|
114
|
+
APICache.logger.debug "Backtrace:\n#{e.backtrace.join("\n")}" unless e.kind_of?(APICacheError)
|
115
|
+
|
57
116
|
if cache_state == :refetch
|
58
|
-
cache.get
|
117
|
+
cache.get
|
59
118
|
else
|
60
|
-
APICache.logger.
|
61
|
-
|
119
|
+
APICache.logger.warn "Data not available in the cache or from API for key #{@key}"
|
120
|
+
if options.has_key?(:fail)
|
121
|
+
fail = options[:fail]
|
122
|
+
fail.respond_to?(:call) ? fail.call : fail
|
123
|
+
else
|
124
|
+
raise e
|
125
|
+
end
|
62
126
|
end
|
63
127
|
end
|
64
128
|
end
|
@@ -67,8 +131,7 @@ end
|
|
67
131
|
|
68
132
|
require 'api_cache/cache'
|
69
133
|
require 'api_cache/api'
|
70
|
-
require 'api_cache/logger'
|
71
134
|
|
72
135
|
APICache.autoload 'AbstractStore', 'api_cache/abstract_store'
|
73
136
|
APICache.autoload 'MemoryStore', 'api_cache/memory_store'
|
74
|
-
APICache.autoload '
|
137
|
+
APICache.autoload 'MonetaStore', 'api_cache/moneta_store'
|
@@ -1,25 +1,27 @@
|
|
1
|
-
class APICache
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
1
|
+
class APICache
|
2
|
+
class AbstractStore
|
3
|
+
def initialize
|
4
|
+
raise "Method not implemented. Called abstract class."
|
5
|
+
end
|
6
|
+
|
7
|
+
# Set value. Returns true if success.
|
8
|
+
def set(key, value)
|
9
|
+
raise "Method not implemented. Called abstract class."
|
10
|
+
end
|
11
|
+
|
12
|
+
# Get value.
|
13
|
+
def get(key)
|
14
|
+
raise "Method not implemented. Called abstract class."
|
15
|
+
end
|
16
|
+
|
17
|
+
# Does a given key exist in the cache?
|
18
|
+
def exists?(key)
|
19
|
+
raise "Method not implemented. Called abstract class."
|
20
|
+
end
|
21
|
+
|
22
|
+
# Has a given time passed since the key was set?
|
23
|
+
def expired?(key, timeout)
|
24
|
+
raise "Method not implemented. Called abstract class."
|
25
|
+
end
|
24
26
|
end
|
25
27
|
end
|
data/lib/api_cache/api.rb
CHANGED
@@ -1,70 +1,97 @@
|
|
1
1
|
require 'net/http'
|
2
2
|
|
3
|
-
|
4
|
-
#
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
3
|
+
class APICache
|
4
|
+
# Wraps up querying the API.
|
5
|
+
#
|
6
|
+
# Ensures that the API is not called more frequently than every +period+
|
7
|
+
# seconds, and times out API requests after +timeout+ seconds.
|
8
|
+
#
|
9
|
+
class API
|
10
|
+
# Takes the following options
|
11
|
+
#
|
12
|
+
# period:: Maximum frequency to call the API. If set to 0 then there is no
|
13
|
+
# limit on how frequently queries can be made to the API.
|
14
|
+
# timeout:: Timeout when calling api (either to the proviced url or
|
15
|
+
# excecuting the passed block)
|
16
|
+
# block:: If passed then the block is excecuted instead of HTTP GET
|
17
|
+
# against the provided key
|
18
|
+
#
|
19
|
+
def initialize(key, options, &block)
|
20
|
+
@key, @block = key, block
|
21
|
+
@timeout = options[:timeout]
|
22
|
+
@period = options[:period]
|
23
|
+
end
|
24
|
+
|
25
|
+
# Fetch data from the API.
|
26
|
+
#
|
27
|
+
# If no block is given then the key is assumed to be a URL and which will
|
28
|
+
# be queried expecting a 200 response. Otherwise the return value of the
|
29
|
+
# block will be used.
|
30
|
+
#
|
31
|
+
# This method can raise Timeout::Error, APICache::InvalidResponse, or any
|
32
|
+
# exception raised in the block passed to APICache.get
|
33
|
+
#
|
34
|
+
def get
|
35
|
+
check_queryable!
|
36
|
+
APICache.logger.debug "Fetching data from the API"
|
37
|
+
set_queried_at
|
38
|
+
Timeout::timeout(@timeout) do
|
39
|
+
if @block
|
40
|
+
# If this call raises an error then the response is not cached
|
41
|
+
@block.call
|
42
|
+
else
|
43
|
+
get_key_via_http
|
44
|
+
end
|
45
|
+
end
|
46
|
+
rescue Timeout::Error => e
|
47
|
+
raise APICache::TimeoutError, "Timed out when calling API (timeout #{@timeout}s)"
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def get_key_via_http
|
53
|
+
response = redirecting_get(@key)
|
54
|
+
case response
|
55
|
+
when Net::HTTPSuccess
|
56
|
+
# 2xx response code
|
57
|
+
response.body
|
20
58
|
else
|
21
|
-
APICache
|
22
|
-
false
|
59
|
+
raise APICache::InvalidResponse, "InvalidResponse http response: #{response.code}"
|
23
60
|
end
|
24
|
-
else
|
25
|
-
APICache.logger.log "Queryable: true - never used API before"
|
26
|
-
true
|
27
61
|
end
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
62
|
+
|
63
|
+
def redirecting_get(url)
|
64
|
+
r = Net::HTTP.get_response(URI.parse(url))
|
65
|
+
r.header['location'] ? redirecting_get(r.header['location']) : r
|
66
|
+
end
|
67
|
+
|
68
|
+
# Checks whether the API can be queried (i.e. whether :period has passed
|
69
|
+
# since the last query to the API).
|
70
|
+
#
|
71
|
+
def check_queryable!
|
72
|
+
if previously_queried?
|
73
|
+
if Time.now - queried_at > @period
|
74
|
+
APICache.logger.debug "Queryable: true - retry_time has passed"
|
75
|
+
else
|
76
|
+
APICache.logger.debug "Queryable: false - queried too recently"
|
77
|
+
raise APICache::CannotFetch,
|
78
|
+
"Cannot fetch #{@key}: queried too recently"
|
79
|
+
end
|
45
80
|
else
|
46
|
-
|
81
|
+
APICache.logger.debug "Queryable: true - never used API before"
|
47
82
|
end
|
48
83
|
end
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
response = redirecting_get(key)
|
57
|
-
case response
|
58
|
-
when Net::HTTPSuccess
|
59
|
-
# 2xx response code
|
60
|
-
response.body
|
61
|
-
else
|
62
|
-
raise APICache::Invalid, "Invalid http response: #{response.code}"
|
84
|
+
|
85
|
+
def previously_queried?
|
86
|
+
APICache.store.exists?("#{@key}_queried_at")
|
87
|
+
end
|
88
|
+
|
89
|
+
def queried_at
|
90
|
+
APICache.store.get("#{@key}_queried_at")
|
63
91
|
end
|
64
|
-
end
|
65
92
|
|
66
|
-
|
67
|
-
|
68
|
-
|
93
|
+
def set_queried_at
|
94
|
+
APICache.store.set("#{@key}_queried_at", Time.now)
|
95
|
+
end
|
69
96
|
end
|
70
97
|
end
|
data/lib/api_cache/cache.rb
CHANGED
@@ -1,41 +1,60 @@
|
|
1
|
-
# Cache performs calculations relating to the status of items stored in the
|
2
|
-
# cache and delegates storage to the various cache stores.
|
3
1
|
require 'digest/md5'
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
2
|
+
|
3
|
+
class APICache
|
4
|
+
# Cache performs calculations relating to the status of items stored in the
|
5
|
+
# cache and delegates storage to the various cache stores.
|
6
|
+
#
|
7
|
+
class Cache
|
8
|
+
# Takes the following options
|
9
|
+
#
|
10
|
+
# cache:: Length of time to cache before re-requesting
|
11
|
+
# valid:: Length of time to consider data still valid if API cannot be
|
12
|
+
# fetched - :forever is a valid option.
|
13
|
+
#
|
14
|
+
def initialize(key, options)
|
15
|
+
@key = key
|
16
|
+
@cache = options[:cache]
|
17
|
+
@valid = options[:valid]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns one of the following options depending on the state of the key:
|
21
|
+
#
|
22
|
+
# * :current (key has been set recently)
|
23
|
+
# * :refetch (data should be refetched but is still available for use)
|
24
|
+
# * :invalid (data is too old to be useful)
|
25
|
+
# * :missing (do data for this key)
|
26
|
+
#
|
27
|
+
def state
|
28
|
+
if store.exists?(hash)
|
29
|
+
if !store.expired?(hash, @cache)
|
30
|
+
:current
|
31
|
+
elsif (@valid == :forever) || !store.expired?(hash, @valid)
|
32
|
+
:refetch
|
33
|
+
else
|
34
|
+
:invalid
|
35
|
+
end
|
22
36
|
else
|
23
|
-
:
|
37
|
+
:missing
|
24
38
|
end
|
25
|
-
else
|
26
|
-
:missing
|
27
39
|
end
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
+
|
41
|
+
def get
|
42
|
+
store.get(hash)
|
43
|
+
end
|
44
|
+
|
45
|
+
def set(value)
|
46
|
+
store.set(hash, value)
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def hash
|
53
|
+
Digest::MD5.hexdigest @key
|
54
|
+
end
|
55
|
+
|
56
|
+
def store
|
57
|
+
APICache.store
|
58
|
+
end
|
40
59
|
end
|
41
60
|
end
|
@@ -1,33 +1,35 @@
|
|
1
|
-
class APICache
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
1
|
+
class APICache
|
2
|
+
class MemoryStore < APICache::AbstractStore
|
3
|
+
def initialize
|
4
|
+
APICache.logger.debug "Using memory store"
|
5
|
+
@cache = {}
|
6
|
+
true
|
7
|
+
end
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
def set(key, value)
|
10
|
+
APICache.logger.debug("cache: set (#{key})")
|
11
|
+
@cache[key] = [Time.now, value]
|
12
|
+
true
|
13
|
+
end
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
def get(key)
|
16
|
+
data = @cache[key][1]
|
17
|
+
APICache.logger.debug("cache: #{data.nil? ? "miss" : "hit"} (#{key})")
|
18
|
+
data
|
19
|
+
end
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
21
|
+
def exists?(key)
|
22
|
+
!@cache[key].nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
def expired?(key, timeout)
|
26
|
+
Time.now - created(key) > timeout
|
27
|
+
end
|
27
28
|
|
28
|
-
|
29
|
+
private
|
29
30
|
|
30
|
-
|
31
|
-
|
31
|
+
def created(key)
|
32
|
+
@cache[key][0]
|
33
|
+
end
|
32
34
|
end
|
33
35
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class APICache
|
2
|
+
class MonetaStore < APICache::AbstractStore
|
3
|
+
def initialize(store)
|
4
|
+
@moneta = store
|
5
|
+
end
|
6
|
+
|
7
|
+
# Set value. Returns true if success.
|
8
|
+
def set(key, value)
|
9
|
+
@moneta[key] = value
|
10
|
+
@moneta["#{key}_created_at"] = Time.now
|
11
|
+
true
|
12
|
+
end
|
13
|
+
|
14
|
+
# Get value.
|
15
|
+
def get(key)
|
16
|
+
@moneta[key]
|
17
|
+
end
|
18
|
+
|
19
|
+
# Does a given key exist in the cache?
|
20
|
+
def exists?(key)
|
21
|
+
@moneta.key?(key)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Has a given time passed since the key was set?
|
25
|
+
def expired?(key, timeout)
|
26
|
+
Time.now - @moneta["#{key}_created_at"] > timeout
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/spec/api_cache_spec.rb
CHANGED
@@ -3,70 +3,99 @@ require File.dirname(__FILE__) + '/spec_helper'
|
|
3
3
|
describe APICache do
|
4
4
|
before :each do
|
5
5
|
@key = 'random_key'
|
6
|
-
@
|
7
|
-
@
|
6
|
+
@cache_data = 'data from the cache'
|
7
|
+
@api_data = 'data from the api'
|
8
8
|
end
|
9
9
|
|
10
|
-
describe "
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
describe "store=" do
|
11
|
+
before :each do
|
12
|
+
APICache.store = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should use APICache::MemoryStore by default" do
|
16
|
+
APICache.store.should be_kind_of(APICache::MemoryStore)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should allow instances of APICache::AbstractStore to be passed" do
|
20
|
+
APICache.store = APICache::MemoryStore.new
|
21
|
+
APICache.store.should be_kind_of(APICache::MemoryStore)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should allow moneta instances to be passed" do
|
25
|
+
require 'moneta'
|
26
|
+
require 'moneta/memory'
|
27
|
+
APICache.store = Moneta::Memory.new
|
28
|
+
APICache.store.should be_kind_of(APICache::MonetaStore)
|
14
29
|
end
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
30
|
+
|
31
|
+
it "should raise an exception if anything else is passed" do
|
32
|
+
lambda {
|
33
|
+
APICache.store = Class
|
34
|
+
}.should raise_error(ArgumentError, 'Please supply an instance of either a moneta store or a subclass of APICache::AbstractStore')
|
35
|
+
end
|
36
|
+
|
37
|
+
after :all do
|
38
|
+
APICache.store = nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "get method" do
|
43
|
+
before :each do
|
44
|
+
@api = mock(APICache::API, :get => @api_data)
|
45
|
+
@cache = mock(APICache::Cache, :get => @cache_data, :set => true)
|
46
|
+
|
47
|
+
APICache::API.stub!(:new).and_return(@api)
|
48
|
+
APICache::Cache.stub!(:new).and_return(@cache)
|
20
49
|
end
|
21
50
|
|
22
51
|
it "should fetch data from the cache if the state is :current" do
|
23
|
-
|
24
|
-
APICache.cache.should_receive(:get).and_return(@data)
|
52
|
+
@cache.stub!(:state).and_return(:current)
|
25
53
|
|
26
|
-
APICache.get(@key).should == @
|
54
|
+
APICache.get(@key).should == @cache_data
|
27
55
|
end
|
28
56
|
|
29
|
-
it "should make new request to API if the state is :refetch" do
|
30
|
-
|
31
|
-
|
57
|
+
it "should make new request to API if the state is :refetch and store result in cache" do
|
58
|
+
@cache.stub!(:state).and_return(:refetch)
|
59
|
+
@cache.should_receive(:set).with(@api_data)
|
32
60
|
|
33
|
-
APICache.get(@key).should == @
|
61
|
+
APICache.get(@key).should == @api_data
|
34
62
|
end
|
35
63
|
|
36
|
-
it "should return the cached value if the
|
37
|
-
|
38
|
-
|
39
|
-
APICache.cache.should_receive(:get).and_return(@data)
|
64
|
+
it "should return the cached value if the state is :refetch but the api is not accessible" do
|
65
|
+
@cache.stub!(:state).and_return(:refetch)
|
66
|
+
@api.should_receive(:get).with.and_raise(APICache::CannotFetch)
|
40
67
|
|
41
|
-
APICache.get(@key).should == @
|
68
|
+
APICache.get(@key).should == @cache_data
|
42
69
|
end
|
43
70
|
|
44
71
|
it "should make new request to API if the state is :invalid" do
|
45
|
-
|
46
|
-
APICache.api.should_receive(:get).with(@key, 5).and_return(@data)
|
72
|
+
@cache.stub!(:state).and_return(:invalid)
|
47
73
|
|
48
|
-
APICache.get(@key).should == @
|
74
|
+
APICache.get(@key).should == @api_data
|
49
75
|
end
|
50
76
|
|
51
|
-
it "should raise
|
52
|
-
|
53
|
-
|
77
|
+
it "should raise CannotFetch if the api cannot fetch data and the cache state is :invalid" do
|
78
|
+
@cache.stub!(:state).and_return(:invalid)
|
79
|
+
@api.should_receive(:get).with.and_raise(APICache::CannotFetch)
|
54
80
|
|
55
|
-
lambda {
|
81
|
+
lambda {
|
82
|
+
APICache.get(@key).should
|
83
|
+
}.should raise_error(APICache::CannotFetch)
|
56
84
|
end
|
57
85
|
|
58
86
|
it "should make new request to API if the state is :missing" do
|
59
|
-
|
60
|
-
APICache.api.should_receive(:get).with(@key, 5).and_return(@data)
|
87
|
+
@cache.stub!(:state).and_return(:missing)
|
61
88
|
|
62
|
-
APICache.get(@key).should == @
|
89
|
+
APICache.get(@key).should == @api_data
|
63
90
|
end
|
64
91
|
|
65
|
-
it "should raise an exception if the api cannot fetch data and state is :missing" do
|
66
|
-
|
67
|
-
|
92
|
+
it "should raise an exception if the api cannot fetch data and the cache state is :missing" do
|
93
|
+
@cache.stub!(:state).and_return(:missing)
|
94
|
+
@api.should_receive(:get).with.and_raise(APICache::CannotFetch)
|
68
95
|
|
69
|
-
lambda {
|
96
|
+
lambda {
|
97
|
+
APICache.get(@key).should
|
98
|
+
}.should raise_error(APICache::CannotFetch)
|
70
99
|
end
|
71
100
|
end
|
72
101
|
end
|
data/spec/api_spec.rb
CHANGED
@@ -1,10 +1,65 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/spec_helper'
|
2
2
|
|
3
3
|
describe APICache::API do
|
4
|
-
|
5
|
-
|
4
|
+
before :each do
|
5
|
+
FakeWeb.register_uri(:get, "http://www.google.com/", :body => "Google")
|
6
|
+
FakeWeb.register_uri(:get, "http://froogle.google.com/", :status => 302, :location => "http://products.google.com")
|
7
|
+
FakeWeb.register_uri(:get, "http://products.google.com/", :body => "Google Product Search")
|
8
|
+
|
9
|
+
@options = {
|
10
|
+
:period => 1,
|
11
|
+
:timeout => 5
|
12
|
+
}
|
13
|
+
|
14
|
+
# Reset the store otherwise get queried too recently erros
|
15
|
+
APICache.store = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should not be queryable for :period seconds after a request" do
|
19
|
+
api = APICache::API.new('http://www.google.com/', @options)
|
20
|
+
|
21
|
+
api.get
|
6
22
|
lambda {
|
7
|
-
api.get
|
8
|
-
}.
|
23
|
+
api.get
|
24
|
+
}.should raise_error(APICache::CannotFetch, "Cannot fetch http://www.google.com/: queried too recently")
|
25
|
+
|
26
|
+
sleep 1
|
27
|
+
lambda {
|
28
|
+
api.get
|
29
|
+
}.should_not raise_error
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "without a block - key is the url" do
|
33
|
+
|
34
|
+
it "should return body of a http GET against the key" do
|
35
|
+
api = APICache::API.new('http://www.google.com/', @options)
|
36
|
+
api.get.should =~ /Google/
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should handle redirecting get requests" do
|
40
|
+
api = APICache::API.new('http://froogle.google.com/', @options)
|
41
|
+
api.get.should =~ /Google Product Search/
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "with a block" do
|
47
|
+
|
48
|
+
it "should return the return value of the block" do
|
49
|
+
api = APICache::API.new('http://www.google.com/', @options) do
|
50
|
+
42
|
51
|
+
end
|
52
|
+
api.get.should == 42
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should return the raised exception if the block raises one" do
|
56
|
+
api = APICache::API.new('foobar', @options) do
|
57
|
+
raise RuntimeError, 'foo'
|
58
|
+
end
|
59
|
+
lambda {
|
60
|
+
api.get
|
61
|
+
}.should raise_error(StandardError, 'foo')
|
62
|
+
end
|
63
|
+
|
9
64
|
end
|
10
|
-
end
|
65
|
+
end
|
data/spec/cache_spec.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe APICache::Cache do
|
4
|
+
before :each do
|
5
|
+
@options = {
|
6
|
+
:cache => 1, # After this time fetch new data
|
7
|
+
:valid => 2 # Maximum time to use old data
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should set and get" do
|
12
|
+
cache = APICache::Cache.new('flubble', @options)
|
13
|
+
|
14
|
+
cache.set('Hello world')
|
15
|
+
cache.get.should == 'Hello world'
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should md5 encode the provided key" do
|
19
|
+
cache = APICache::Cache.new('test_md5', @options)
|
20
|
+
APICache.store.should_receive(:set).
|
21
|
+
with('9050bddcf415f2d0518804e551c1be98', 'md5ing?')
|
22
|
+
cache.set('md5ing?')
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should report correct invalid states" do
|
26
|
+
cache = APICache::Cache.new('foo', @options)
|
27
|
+
|
28
|
+
cache.state.should == :missing
|
29
|
+
cache.set('foo')
|
30
|
+
cache.state.should == :current
|
31
|
+
sleep 1
|
32
|
+
cache.state.should == :refetch
|
33
|
+
sleep 1
|
34
|
+
cache.state.should == :invalid
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe "api_cache" do
|
4
|
+
before :each do
|
5
|
+
FakeWeb.register_uri(:get, "http://www.google.com/", :body => "Google")
|
6
|
+
|
7
|
+
APICache.store = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should work when url supplied" do
|
11
|
+
APICache.get('http://www.google.com/').should =~ /Google/
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should work when block supplied" do
|
15
|
+
APICache.get('foobar') do
|
16
|
+
42
|
17
|
+
end.should == 42
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should raise error raised in block unless key available in cache" do
|
21
|
+
lambda {
|
22
|
+
APICache.get('foo') do
|
23
|
+
raise RuntimeError, 'foo'
|
24
|
+
end
|
25
|
+
}.should raise_error(RuntimeError, 'foo')
|
26
|
+
|
27
|
+
APICache.get('foo', :period => 0) do
|
28
|
+
'bar'
|
29
|
+
end
|
30
|
+
|
31
|
+
lambda {
|
32
|
+
APICache.get('foo') do
|
33
|
+
raise RuntimeError, 'foo'
|
34
|
+
end
|
35
|
+
}.should_not raise_error
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should raise APICache::TimeoutError if the API call times out unless data available in cache" do
|
39
|
+
lambda {
|
40
|
+
APICache.get('foo', :timeout => 1) do
|
41
|
+
sleep 1.1
|
42
|
+
end
|
43
|
+
}.should raise_error APICache::TimeoutError, 'Timed out when calling API (timeout 1s)'
|
44
|
+
|
45
|
+
APICache.get('foo', :period => 0) do
|
46
|
+
'bar'
|
47
|
+
end
|
48
|
+
|
49
|
+
lambda {
|
50
|
+
APICache.get('foo', :timeout => 1) do
|
51
|
+
sleep 1.1
|
52
|
+
end
|
53
|
+
}.should_not raise_error
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should return a default value rather than raising an exception if :fail passed" do
|
57
|
+
APICache.get('foo', :fail => "bar") do
|
58
|
+
raise 'foo'
|
59
|
+
end.should == 'bar'
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should accept a proc to fail" do
|
63
|
+
APICache.get('foo', :fail => lambda { "bar" }) do
|
64
|
+
raise 'foo'
|
65
|
+
end.should == 'bar'
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should accept nil values for :fail" do
|
69
|
+
APICache.get('foo', :fail => nil) do
|
70
|
+
raise 'foo'
|
71
|
+
end.should == nil
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
require 'moneta/memcache'
|
4
|
+
|
5
|
+
describe APICache::MonetaStore do
|
6
|
+
before :each do
|
7
|
+
@moneta = Moneta::Memcache.new(:server => "localhost")
|
8
|
+
@moneta.delete('foo')
|
9
|
+
@store = APICache::MonetaStore.new(@moneta)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should set and get" do
|
13
|
+
@store.set("key", "value")
|
14
|
+
@store.get("key").should == "value"
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should allow checking whether a key exists" do
|
18
|
+
@store.exists?('foo').should be_false
|
19
|
+
@store.set('foo', 'bar')
|
20
|
+
@store.exists?('foo').should be_true
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should allow checking whether a given amount of time has passed since the key was set" do
|
24
|
+
@store.expired?('foo', 1).should be_false
|
25
|
+
@store.set('foo', 'bar')
|
26
|
+
@store.expired?('foo', 1).should be_false
|
27
|
+
sleep 1
|
28
|
+
@store.expired?('foo', 1).should be_true
|
29
|
+
end
|
30
|
+
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mloughran-api_cache
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Martyn Loughran
|
@@ -9,11 +9,30 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-08-21 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
|
-
dependencies:
|
15
|
-
|
16
|
-
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: fakeweb
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
description: APICache allows any API client library to be easily wrapped with a robust caching layer. It supports caching (obviously), serving stale data and limits on the number of API calls. It's also got a handy syntax if all you want to do is cache a bothersome url.
|
17
36
|
email: me@mloughran.com
|
18
37
|
executables: []
|
19
38
|
|
@@ -22,21 +41,24 @@ extensions: []
|
|
22
41
|
extra_rdoc_files: []
|
23
42
|
|
24
43
|
files:
|
25
|
-
- README.
|
44
|
+
- README.rdoc
|
26
45
|
- VERSION.yml
|
27
46
|
- lib/api_cache
|
28
47
|
- lib/api_cache/abstract_store.rb
|
29
48
|
- lib/api_cache/api.rb
|
30
49
|
- lib/api_cache/cache.rb
|
31
|
-
- lib/api_cache/logger.rb
|
32
|
-
- lib/api_cache/memcache_store.rb
|
33
50
|
- lib/api_cache/memory_store.rb
|
51
|
+
- lib/api_cache/moneta_store.rb
|
34
52
|
- lib/api_cache.rb
|
35
53
|
- spec/api_cache_spec.rb
|
36
54
|
- spec/api_spec.rb
|
55
|
+
- spec/cache_spec.rb
|
56
|
+
- spec/integration_spec.rb
|
57
|
+
- spec/monteta_store_spec.rb
|
37
58
|
- spec/spec_helper.rb
|
38
|
-
has_rdoc:
|
39
|
-
homepage: http://github.com/
|
59
|
+
has_rdoc: false
|
60
|
+
homepage: http://mloughran.github.com/api_cache/
|
61
|
+
licenses:
|
40
62
|
post_install_message:
|
41
63
|
rdoc_options:
|
42
64
|
- --inline-source
|
@@ -58,9 +80,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
80
|
requirements: []
|
59
81
|
|
60
82
|
rubyforge_project:
|
61
|
-
rubygems_version: 1.
|
83
|
+
rubygems_version: 1.3.5
|
62
84
|
signing_key:
|
63
|
-
specification_version:
|
64
|
-
summary:
|
85
|
+
specification_version: 3
|
86
|
+
summary: API Cache allows advanced caching of APIs
|
65
87
|
test_files: []
|
66
88
|
|
data/README.markdown
DELETED
@@ -1,80 +0,0 @@
|
|
1
|
-
APICache (aka api_cache)
|
2
|
-
========================
|
3
|
-
|
4
|
-
For the impatient
|
5
|
-
-----------------
|
6
|
-
|
7
|
-
# Install
|
8
|
-
sudo gem install mloughran-api_cache -s http://gems.github.com
|
9
|
-
|
10
|
-
# Require
|
11
|
-
require 'rubygems'
|
12
|
-
gem 'mloughran-api_cache'
|
13
|
-
require 'api_cache'
|
14
|
-
|
15
|
-
# Configure
|
16
|
-
APICache.start(APICache::MemoryStore)
|
17
|
-
|
18
|
-
# Use
|
19
|
-
APICache.get("http://twitter.com/statuses/public_timeline.rss")
|
20
|
-
|
21
|
-
For everyone else
|
22
|
-
-----------------
|
23
|
-
|
24
|
-
You want to use the Twitter API but you don't want to die? I have the solution to API caching:
|
25
|
-
|
26
|
-
APICache.get("http://twitter.com/statuses/public_timeline.rss")
|
27
|
-
|
28
|
-
You get the following functionality for free:
|
29
|
-
|
30
|
-
* New data every 10 minutes
|
31
|
-
* If the twitter API dies then keep using the last data received for a day. Then assume it's invalid and announce that Twitter has FAILED (optional).
|
32
|
-
* Don't hit the rate limit (70 requests per 60 minutes)
|
33
|
-
|
34
|
-
So what exactly does `APICache` do? Given cached data less than 10 minutes old, it returns that. Otherwise, assuming it didn't try to request the URL within the last minute (to avoid the rate limit), it makes a get request to the Twitter API. If the Twitter API timeouts or doesn't return a 2xx code (very likely) we're still fine: it just returns the last data fetched (as long as it's less than a day old). In the exceptional case that all is lost and no data can be returned, it raises an `APICache::NotAvailableError` exception. You're responsible for catching this exception and complaining bitterly to the internet.
|
35
|
-
|
36
|
-
All very simple. What if you need to do something more complicated? Say you need authentication or the silly API you're using doesn't follow a nice convention of returning 2xx for success. Then you need a block:
|
37
|
-
|
38
|
-
APICache.get('twitter_replies', :cache => 3600) do
|
39
|
-
Net::HTTP.start('twitter.com') do |http|
|
40
|
-
req = Net::HTTP::Get.new('/statuses/replies.xml')
|
41
|
-
req.basic_auth 'username', 'password'
|
42
|
-
response = http.request(req)
|
43
|
-
case response
|
44
|
-
when Net::HTTPSuccess
|
45
|
-
# 2xx response code
|
46
|
-
response.body
|
47
|
-
else
|
48
|
-
raise APICache::Invalid
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
All the caching is still handled for you. If you supply a block then the first argument to `APICache.get` is assumed to be a unique key rather than a URL. Throwing `APICache::Invalid` signals to `APICache` that the request was not successful.
|
54
|
-
|
55
|
-
You can send any of the following options to `APICache.get(url, options = {}, &block)`. These are the default values (times are all in seconds):
|
56
|
-
|
57
|
-
{
|
58
|
-
:cache => 600, # 10 minutes After this time fetch new data
|
59
|
-
:valid => 86400, # 1 day Maximum time to use old data
|
60
|
-
# :forever is a valid option
|
61
|
-
:period => 60, # 1 minute Maximum frequency to call API
|
62
|
-
:timeout => 5 # 5 seconds API response timeout
|
63
|
-
}
|
64
|
-
|
65
|
-
Before using the APICache you need to initialize the caches. In merb, for example, put this in your `init.rb`:
|
66
|
-
|
67
|
-
APICache.start
|
68
|
-
|
69
|
-
Currently there are two stores available: `MemcacheStore` and `MemoryStore`. `MemcacheStore` is the default but if you'd like to use `MemoryStore`, or another store - see `AbstractStore`, just supply it to the start method:
|
70
|
-
|
71
|
-
APICache.start(APICache::MemoryStore)
|
72
|
-
|
73
|
-
I suppose you'll want to get your hands on this magic! Just take a look at the instructions above for the impatient. Well done for reading this first!
|
74
|
-
|
75
|
-
Please send feedback to me [at] mloughran [dot] com if you think of any other functionality that would be handy.
|
76
|
-
|
77
|
-
Copyright
|
78
|
-
=========
|
79
|
-
|
80
|
-
Copyright (c) 2008 Martyn Loughran. See LICENSE for details.
|
data/lib/api_cache/logger.rb
DELETED
@@ -1,54 +0,0 @@
|
|
1
|
-
require 'memcache'
|
2
|
-
|
3
|
-
class APICache::MemcacheStore < APICache::AbstractStore
|
4
|
-
class NotReady < Exception #:nodoc:
|
5
|
-
def initialize
|
6
|
-
super("Memcache server is not ready")
|
7
|
-
end
|
8
|
-
end
|
9
|
-
|
10
|
-
class NotDefined < Exception #:nodoc:
|
11
|
-
def initialize
|
12
|
-
super("Memcache is not defined")
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def initialize
|
17
|
-
APICache.logger.log "Using memcached store"
|
18
|
-
namespace = 'api_cache'
|
19
|
-
host = '127.0.0.1:11211'
|
20
|
-
@memcache = MemCache.new(host, {:namespace => namespace})
|
21
|
-
raise NotReady unless @memcache.active?
|
22
|
-
true
|
23
|
-
# rescue NameError
|
24
|
-
# raise NotDefined
|
25
|
-
end
|
26
|
-
|
27
|
-
def set(key, data)
|
28
|
-
@memcache.set(key, data)
|
29
|
-
@memcache.set("#{key}_created_at", Time.now)
|
30
|
-
APICache.logger.log("cache: set (#{key})")
|
31
|
-
true
|
32
|
-
end
|
33
|
-
|
34
|
-
def get(key)
|
35
|
-
data = @memcache.get(key)
|
36
|
-
APICache.logger.log("cache: #{data.nil? ? "miss" : "hit"} (#{key})")
|
37
|
-
data
|
38
|
-
end
|
39
|
-
|
40
|
-
def exists?(key)
|
41
|
-
# TODO: inefficient - is there a better way?
|
42
|
-
!@memcache.get(key).nil?
|
43
|
-
end
|
44
|
-
|
45
|
-
def expired?(key, timeout)
|
46
|
-
Time.now - created(key) > timeout
|
47
|
-
end
|
48
|
-
|
49
|
-
private
|
50
|
-
|
51
|
-
def created(key)
|
52
|
-
@memcache.get("#{key}_created_at")
|
53
|
-
end
|
54
|
-
end
|