Skip to content

Commit 0fd0dca

Browse files
author
Sergio Gomes
committed
Adding discovery document caching to Service interface
1 parent 0d7b3d0 commit 0fd0dca

File tree

4 files changed

+349
-15
lines changed

4 files changed

+349
-15
lines changed

lib/google/api_client/service.rb

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
require 'google/api_client/service/request'
1919
require 'google/api_client/service/result'
2020
require 'google/api_client/service/batch'
21+
require 'google/api_client/service/simple_file_store'
2122

2223
module Google
2324
class APIClient
@@ -33,6 +34,8 @@ class Service
3334
include Google::APIClient::Service::StubGenerator
3435
extend Forwardable
3536

37+
DEFAULT_CACHE_FILE = 'discovery.cache'
38+
3639
# Cache for discovered APIs.
3740
@@discovered = {}
3841

@@ -81,6 +84,10 @@ class Service
8184
# `true` if gzip enabled, `false` otherwise.
8285
# @option options [Faraday::Connection] :connection
8386
# A custom connection to be used for all requests.
87+
# @option options [ActiveSupport::Cache::Store, :default] :discovery_cache
88+
# A cache store to place the discovery documents for loaded APIs.
89+
# Avoids unnecessary roundtrips to the discovery service.
90+
# :default loads the default local file cache store.
8491
def initialize(api_name, api_version, options = {})
8592
@api_name = api_name.to_s
8693
if api_version.nil?
@@ -109,11 +116,32 @@ def initialize(api_name, api_version, options = {})
109116

110117
@options = options
111118

112-
# Cache discovered APIs in memory.
119+
# Initialize cache store. Default to SimpleFileStore if :cache_store
120+
# is not provided and we have write permissions.
121+
if options.include? :cache_store
122+
@cache_store = options[:cache_store]
123+
else
124+
cache_exists = File.exist?(DEFAULT_CACHE_FILE)
125+
if (cache_exists && File.writable?(DEFAULT_CACHE_FILE)) ||
126+
(!cache_exists && File.writable?(Dir.pwd))
127+
@cache_store = Google::APIClient::Service::SimpleFileStore.new(
128+
DEFAULT_CACHE_FILE)
129+
end
130+
end
131+
132+
# Attempt to read API definition from memory cache.
113133
# Not thread-safe, but the worst that can happen is a cache miss.
114134
unless @api = @@discovered[[api_name, api_version]]
115-
@@discovered[[api_name, api_version]] = @api = @client.discovered_api(
116-
api_name, api_version)
135+
# Attempt to read API definition from cache store, if there is one.
136+
# If there's a miss or no cache store, call discovery service.
137+
if !@cache_store.nil?
138+
@api = @cache_store.fetch("%s/%s" % [api_name, api_version]) do
139+
@client.discovered_api(api_name, api_version)
140+
end
141+
else
142+
@api = @client.discovered_api(api_name, api_version)
143+
end
144+
@@discovered[[api_name, api_version]] = @api
117145
end
118146

119147
generate_call_stubs(self, @api)
@@ -151,6 +179,16 @@ def initialize(api_name, api_version, options = {})
151179
# @return [Faraday::Connection]
152180
attr_accessor :connection
153181

182+
##
183+
# The cache store used for storing discovery documents.
184+
# If the user requested :default, use SimpleFileStore with default file
185+
# name.
186+
#
187+
# @return [ActiveSupport::Cache::Store,
188+
# Google::APIClient::Service::SimpleFileStore,
189+
# nil]
190+
attr_reader :cache_store
191+
154192
##
155193
# Prepares a Google::APIClient::BatchRequest object to make batched calls.
156194
# @param [Array] calls
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# Copyright 2013 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
module Google
16+
class APIClient
17+
class Service
18+
19+
# Simple file store to be used in the event no ActiveSupport cache store
20+
# is provided. This is not thread-safe, and does not support a number of
21+
# features (such as expiration), but it's useful for the simple purpose of
22+
# caching discovery documents to disk.
23+
# Implements the basic cache methods of ActiveSupport::Cache::Store in a
24+
# limited fashion.
25+
class SimpleFileStore
26+
27+
# Creates a new SimpleFileStore.
28+
#
29+
# @param [String] file_path
30+
# The path to the cache file on disk.
31+
# @param [Object] options
32+
# The options to be used with this SimpleFileStore. Not implemented.
33+
def initialize(file_path, options = nil)
34+
@file_path = file_path.to_s
35+
end
36+
37+
# Returns true if a key exists in the cache.
38+
#
39+
# @param [String] name
40+
# The name of the key. Will always be converted to a string.
41+
# @param [Object] options
42+
# The options to be used with this query. Not implemented.
43+
def exist?(name, options = nil)
44+
read_file
45+
@cache.nil? ? nil : @cache.include?(name.to_s)
46+
end
47+
48+
# Fetches data from the cache and returns it, using the given key.
49+
# If the key is missing and no block is passed, returns nil.
50+
# If the key is missing and a block is passed, executes the block, sets
51+
# the key to its value, and returns it.
52+
#
53+
# @param [String] name
54+
# The name of the key. Will always be converted to a string.
55+
# @param [Object] options
56+
# The options to be used with this query. Not implemented.
57+
# @yield [String]
58+
# optional block with the default value if the key is missing
59+
def fetch(name, options = nil)
60+
read_file
61+
if block_given?
62+
entry = read(name.to_s, options)
63+
if entry.nil?
64+
value = yield name.to_s
65+
write(name.to_s, value)
66+
return value
67+
else
68+
return entry
69+
end
70+
else
71+
return read(name.to_s, options)
72+
end
73+
end
74+
75+
# Fetches data from the cache, using the given key.
76+
# Returns nil if the key is missing.
77+
#
78+
# @param [String] name
79+
# The name of the key. Will always be converted to a string.
80+
# @param [Object] options
81+
# The options to be used with this query. Not implemented.
82+
def read(name, options = nil)
83+
read_file
84+
@cache.nil? ? nil : @cache[name.to_s]
85+
end
86+
87+
# Writes the value to the cache, with the key.
88+
#
89+
# @param [String] name
90+
# The name of the key. Will always be converted to a string.
91+
# @param [Object] value
92+
# The value to be written.
93+
# @param [Object] options
94+
# The options to be used with this query. Not implemented.
95+
def write(name, value, options = nil)
96+
read_file
97+
@cache = {} if @cache.nil?
98+
@cache[name.to_s] = value
99+
write_file
100+
return nil
101+
end
102+
103+
# Deletes an entry in the cache.
104+
# Returns true if an entry is deleted.
105+
#
106+
# @param [String] name
107+
# The name of the key. Will always be converted to a string.
108+
# @param [Object] options
109+
# The options to be used with this query. Not implemented.
110+
def delete(name, options = nil)
111+
read_file
112+
return nil if @cache.nil?
113+
if @cache.include? name.to_s
114+
@cache.delete name.to_s
115+
write_file
116+
return true
117+
else
118+
return nil
119+
end
120+
end
121+
122+
protected
123+
124+
# Read the entire cache file from disk.
125+
# Will avoid reading if there have been no changes.
126+
def read_file
127+
if !File.exists? @file_path
128+
@cache = nil
129+
else
130+
# Check for changes after our last read or write.
131+
if @last_change.nil? || File.mtime(@file_path) > @last_change
132+
File.open(@file_path) do |file|
133+
@cache = Marshal.load(file)
134+
@last_change = file.mtime
135+
end
136+
end
137+
end
138+
return @cache
139+
end
140+
141+
# Write the entire cache contents to disk.
142+
def write_file
143+
File.open(@file_path, 'w') do |file|
144+
Marshal.dump(@cache, file)
145+
end
146+
@last_change = File.mtime(@file_path)
147+
end
148+
end
149+
end
150+
end
151+
end

spec/google/api_client/service_spec.rb

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@
5757
{
5858
:application_name => APPLICATION_NAME,
5959
:authenticated => false,
60-
:connection => conn
60+
:connection => conn,
61+
:cache_store => nil
6162
}
6263
)
6364

@@ -76,7 +77,8 @@
7677
{
7778
:application_name => APPLICATION_NAME,
7879
:authenticated => false,
79-
:connection => conn
80+
:connection => conn,
81+
:cache_store => nil
8082
}
8183
)
8284
req = adsense.adunits.list(:adClientId => '1').execute()
@@ -93,7 +95,8 @@
9395
{
9496
:application_name => APPLICATION_NAME,
9597
:authenticated => false,
96-
:connection => conn
98+
:connection => conn,
99+
:cache_store => nil
97100
}
98101
)
99102
req = adsense.accounts.adclients.list(:accountId => '1').execute()
@@ -102,7 +105,7 @@
102105
describe 'with no connection' do
103106
before do
104107
@adsense = Google::APIClient::Service.new('adsense', 'v1.3',
105-
{:application_name => APPLICATION_NAME})
108+
{:application_name => APPLICATION_NAME, :cache_store => nil})
106109
end
107110

108111
it 'should return a resource when using a valid resource name' do
@@ -152,7 +155,8 @@
152155
{
153156
:application_name => APPLICATION_NAME,
154157
:authenticated => false,
155-
:connection => conn
158+
:connection => conn,
159+
:cache_store => nil
156160
}
157161
)
158162
req = prediction.trainedmodels.insert(:project => '1').body({'id' => '1'}).execute()
@@ -171,7 +175,8 @@
171175
{
172176
:application_name => APPLICATION_NAME,
173177
:authenticated => false,
174-
:connection => conn
178+
:connection => conn,
179+
:cache_store => nil
175180
}
176181
)
177182
req = prediction.trainedmodels.insert(:project => '1').body('{"id":"1"}').execute()
@@ -181,7 +186,7 @@
181186
describe 'with no connection' do
182187
before do
183188
@prediction = Google::APIClient::Service.new('prediction', 'v1.5',
184-
{:application_name => APPLICATION_NAME})
189+
{:application_name => APPLICATION_NAME, :cache_store => nil})
185190
end
186191

187192
it 'should return a valid request with a body' do
@@ -227,7 +232,8 @@
227232
{
228233
:application_name => APPLICATION_NAME,
229234
:authenticated => false,
230-
:connection => conn
235+
:connection => conn,
236+
:cache_store => nil
231237
}
232238
)
233239
req = drive.files.insert(:uploadType => 'multipart').body(@metadata).media(@media).execute()
@@ -237,7 +243,7 @@
237243
describe 'with no connection' do
238244
before do
239245
@drive = Google::APIClient::Service.new('drive', 'v1',
240-
{:application_name => APPLICATION_NAME})
246+
{:application_name => APPLICATION_NAME, :cache_store => nil})
241247
end
242248

243249
it 'should return a valid request with a body and media upload' do
@@ -265,7 +271,8 @@
265271
describe 'with the Discovery API' do
266272
it 'should make a valid end-to-end request' do
267273
discovery = Google::APIClient::Service.new('discovery', 'v1',
268-
{:application_name => APPLICATION_NAME, :authenticated => false})
274+
{:application_name => APPLICATION_NAME, :authenticated => false,
275+
:cache_store => nil})
269276
result = discovery.apis.get_rest(:api => 'discovery', :version => 'v1').execute
270277
result.should_not be_nil
271278
result.data.name.should == 'discovery'
@@ -280,7 +287,7 @@
280287
describe 'with the plus API' do
281288
before do
282289
@plus = Google::APIClient::Service.new('plus', 'v1',
283-
{:application_name => APPLICATION_NAME})
290+
{:application_name => APPLICATION_NAME, :cache_store => nil})
284291
@reference = Google::APIClient::Reference.new({
285292
:api_method => @plus.activities.list.method,
286293
:parameters => {
@@ -478,7 +485,8 @@
478485
describe 'with the discovery API' do
479486
before do
480487
@discovery = Google::APIClient::Service.new('discovery', 'v1',
481-
{:application_name => APPLICATION_NAME, :authorization => nil})
488+
{:application_name => APPLICATION_NAME, :authorization => nil,
489+
:cache_store => nil})
482490
end
483491

484492
describe 'with two valid requests' do

0 commit comments

Comments
 (0)