appengine-apis 0.0.2 → 0.0.3
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/History.txt +6 -0
- data/Manifest.txt +6 -0
- data/README.rdoc +11 -1
- data/Rakefile +8 -0
- data/lib/appengine-apis.rb +1 -1
- data/lib/appengine-apis/apiproxy.rb +2 -2
- data/lib/appengine-apis/datastore.rb +10 -11
- data/lib/appengine-apis/datastore_types.rb +11 -3
- data/lib/appengine-apis/local_boot.rb +29 -0
- data/lib/appengine-apis/mail.rb +160 -0
- data/lib/appengine-apis/memcache.rb +580 -0
- data/lib/appengine-apis/merb-logger.rb +1 -0
- data/lib/appengine-apis/sdk.rb +93 -0
- data/lib/appengine-apis/testing.rb +55 -10
- data/lib/appengine-apis/urlfetch.rb +7 -2
- data/spec/datastore_types_spec.rb +12 -1
- data/spec/mail_spec.rb +179 -0
- data/spec/memcache_spec.rb +385 -0
- data/spec/spec.opts +1 -1
- data/spec/spec_helper.rb +75 -0
- metadata +15 -7
data/History.txt
CHANGED
data/Manifest.txt
CHANGED
@@ -7,8 +7,12 @@ lib/appengine-apis.rb
|
|
7
7
|
lib/appengine-apis/apiproxy.rb
|
8
8
|
lib/appengine-apis/datastore.rb
|
9
9
|
lib/appengine-apis/datastore_types.rb
|
10
|
+
lib/appengine-apis/local_boot.rb
|
10
11
|
lib/appengine-apis/logger.rb
|
12
|
+
lib/appengine-apis/mail.rb
|
13
|
+
lib/appengine-apis/memcache.rb
|
11
14
|
lib/appengine-apis/merb-logger.rb
|
15
|
+
lib/appengine-apis/sdk.rb
|
12
16
|
lib/appengine-apis/testing.rb
|
13
17
|
lib/appengine-apis/urlfetch.rb
|
14
18
|
lib/appengine-apis/users.rb
|
@@ -18,6 +22,8 @@ script/generate
|
|
18
22
|
spec/datastore_spec.rb
|
19
23
|
spec/datastore_types_spec.rb
|
20
24
|
spec/logger_spec.rb
|
25
|
+
spec/mail_spec.rb
|
26
|
+
spec/memcache_spec.rb
|
21
27
|
spec/spec.opts
|
22
28
|
spec/spec_helper.rb
|
23
29
|
spec/urlfetch_spec.rb
|
data/README.rdoc
CHANGED
@@ -6,10 +6,20 @@
|
|
6
6
|
|
7
7
|
APIs and utilities for using JRuby on Google App Engine.
|
8
8
|
|
9
|
+
To load the API stubs in IRB simply
|
10
|
+
require 'rubygems'
|
11
|
+
require 'appengine-apis/local_boot'
|
12
|
+
|
13
|
+
This will configure access to the same Datastore as running
|
14
|
+
|
15
|
+
$ dev_appserver.sh .
|
16
|
+
|
9
17
|
See these classes for an overview of each API:
|
10
18
|
- AppEngine::Logger
|
11
19
|
- AppEngine::Testing
|
12
20
|
- AppEngine::Users
|
21
|
+
- AppEngine::Mail
|
22
|
+
- AppEngine::Memcache
|
13
23
|
- AppEngine::URLFetch
|
14
24
|
- AppEngine::Datastore
|
15
25
|
|
@@ -28,7 +38,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
|
|
28
38
|
you may not use this file except in compliance with the License.
|
29
39
|
You may obtain a copy of the License at
|
30
40
|
|
31
|
-
|
41
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
32
42
|
|
33
43
|
Unless required by applicable law or agreed to in writing, software
|
34
44
|
distributed under the License is distributed on an "AS IS" BASIS,
|
data/Rakefile
CHANGED
@@ -1,6 +1,14 @@
|
|
1
1
|
%w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
|
2
2
|
require File.dirname(__FILE__) + '/lib/appengine-apis'
|
3
3
|
|
4
|
+
# set up pretty rdoc if possible
|
5
|
+
begin
|
6
|
+
gem 'rdoc'
|
7
|
+
require 'sdoc'
|
8
|
+
ENV['RDOCOPT'] = '-T lightblue'
|
9
|
+
rescue
|
10
|
+
end
|
11
|
+
|
4
12
|
# Generate all the Rake tasks
|
5
13
|
# Run 'rake -T' to see list of generated tasks (from gem root directory)
|
6
14
|
$hoe = Hoe.new('appengine-apis', AppEngine::VERSION) do |p|
|
data/lib/appengine-apis.rb
CHANGED
@@ -18,11 +18,11 @@
|
|
18
18
|
#
|
19
19
|
# Ruby interface to the Java ApiProxy.
|
20
20
|
|
21
|
-
require '
|
21
|
+
require 'appengine-apis/sdk'
|
22
22
|
|
23
23
|
module AppEngine
|
24
24
|
|
25
|
-
|
25
|
+
ApiProxy = AppEngine::SDK.load_apiproxy
|
26
26
|
|
27
27
|
class << ApiProxy
|
28
28
|
def get_app_id
|
@@ -24,7 +24,6 @@
|
|
24
24
|
#
|
25
25
|
# The datastore errors are defined in datastore_types.rb.
|
26
26
|
|
27
|
-
require 'appengine-apis/apiproxy'
|
28
27
|
require 'appengine-apis/datastore_types'
|
29
28
|
|
30
29
|
module AppEngine
|
@@ -60,7 +59,7 @@ module AppEngine
|
|
60
59
|
# - Datastore::Text
|
61
60
|
# - Datastore::Blob
|
62
61
|
# - Datastore::ByteString
|
63
|
-
# -
|
62
|
+
# - Users::User
|
64
63
|
|
65
64
|
module Datastore
|
66
65
|
module_function
|
@@ -145,15 +144,15 @@ module Datastore
|
|
145
144
|
# current transaction and will be returned by subsequent, same-thread
|
146
145
|
# calls to #current_transaction until one of the following happens:
|
147
146
|
#
|
148
|
-
#
|
149
|
-
#
|
150
|
-
#
|
151
|
-
#
|
152
|
-
#
|
153
|
-
#
|
154
|
-
#
|
155
|
-
#
|
156
|
-
#
|
147
|
+
# 1. begin_transaction is invoked from the same thread. In this case
|
148
|
+
# current_transaction will return the result of the more recent
|
149
|
+
# call to begin_transaction.
|
150
|
+
# 2. Transaction.commit is invoked on the Transaction returned by
|
151
|
+
# this method. Whether or not the commit succeeds, the
|
152
|
+
# Transaction will no longer be current.
|
153
|
+
# 3. Transaction.rollback is invoked on the Transaction returned by
|
154
|
+
# this method. Whether or not the rollback succeeds, the
|
155
|
+
# Transaction will no longer be current.
|
157
156
|
#
|
158
157
|
def begin_transaction
|
159
158
|
convert_exceptions do
|
@@ -125,7 +125,7 @@ module AppEngine
|
|
125
125
|
# across all apps, and includes all information necessary to fetch
|
126
126
|
# the entity from the datastore with #Datastore.get(Key).
|
127
127
|
#
|
128
|
-
# See http://code.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Key.html
|
128
|
+
# See also http://code.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Key.html
|
129
129
|
#
|
130
130
|
class Key
|
131
131
|
|
@@ -227,7 +227,7 @@ module AppEngine
|
|
227
227
|
# as an arbitrary string), and a set of zero or more typed
|
228
228
|
# properties.
|
229
229
|
#
|
230
|
-
# See http://code.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Entity.html
|
230
|
+
# See also http://code.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Entity.html
|
231
231
|
#
|
232
232
|
class Entity
|
233
233
|
include Enumerable
|
@@ -306,7 +306,7 @@ module AppEngine
|
|
306
306
|
# Add the properties from +other+ to this Entity.
|
307
307
|
# Other may be an Entity or Hash
|
308
308
|
def update(other)
|
309
|
-
|
309
|
+
other.each do |name, value|
|
310
310
|
self[name] = value
|
311
311
|
end
|
312
312
|
self
|
@@ -317,6 +317,14 @@ module AppEngine
|
|
317
317
|
def each(&proc) # :yields: name, value
|
318
318
|
getProperties.each(&proc)
|
319
319
|
end
|
320
|
+
|
321
|
+
def to_hash
|
322
|
+
inject({}) do |hash, item|
|
323
|
+
name, value = item
|
324
|
+
hash[name] = value
|
325
|
+
hash
|
326
|
+
end
|
327
|
+
end
|
320
328
|
end
|
321
329
|
|
322
330
|
SPECIAL_RUBY_TYPES = [Time, Text, Blob, ByteString, Link].freeze
|
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/ruby1.8 -w
|
2
|
+
#
|
3
|
+
# Copyright:: Copyright 2009 Google Inc.
|
4
|
+
# Original Author:: Ryan Brown (mailto:ribrdb@google.com)
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
#
|
19
|
+
# Helpers for installing stub apis in unit tests.
|
20
|
+
|
21
|
+
require 'appengine-apis/testing'
|
22
|
+
AppEngine::Testing.boot
|
23
|
+
|
24
|
+
require 'appengine-apis/datastore'
|
25
|
+
require 'appengine-apis/logger'
|
26
|
+
require 'appengine-apis/mail'
|
27
|
+
require 'appengine-apis/memcache'
|
28
|
+
require 'appengine-apis/urlfetch'
|
29
|
+
require 'appengine-apis/users'
|
@@ -0,0 +1,160 @@
|
|
1
|
+
#!/usr/bin/ruby1.8 -w
|
2
|
+
#
|
3
|
+
# Copyright:: Copyright 2009 Google Inc.
|
4
|
+
# Original Author:: Ryan Brown (mailto:ribrdb@google.com)
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
module AppEngine
|
20
|
+
|
21
|
+
# App Engine applications can send email messages on behalf of the app's
|
22
|
+
# administrators, and on behalf of users with Google Accounts. Apps use the
|
23
|
+
# Mail service to send email messages.
|
24
|
+
#
|
25
|
+
# The Mail.send method sends an email message from the application. The From:
|
26
|
+
# address can be either the email address of a registered administrator
|
27
|
+
# (developer) of the application, or the current user if signed in with
|
28
|
+
# Google Accounts.
|
29
|
+
#
|
30
|
+
# The following example sends an email message to the user as confirmation
|
31
|
+
# that the user created a new account with the application:
|
32
|
+
#
|
33
|
+
# class SignupController < Merb::Controller
|
34
|
+
# def confirm(self):
|
35
|
+
# user_address = params[:email_address"]
|
36
|
+
# confirmation_url = create_new_user_confirmation
|
37
|
+
# sender_address = "support@example.com"
|
38
|
+
# subject = "Confirm your registration"
|
39
|
+
# body = <<EOM
|
40
|
+
# Thank you for creating an account! Please confirm your email address by
|
41
|
+
# clicking on the link below:
|
42
|
+
#
|
43
|
+
# #{confirmation_url}
|
44
|
+
# EOM
|
45
|
+
#
|
46
|
+
# AppEngine::Mail.send(sender_address, user_address, subject, body)
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
module Mail
|
50
|
+
import com.google.appengine.api.mail.MailServiceFactory
|
51
|
+
import com.google.appengine.api.mail.MailService
|
52
|
+
|
53
|
+
module_function
|
54
|
+
|
55
|
+
# Sends an email.
|
56
|
+
#
|
57
|
+
# The message will be delivered asynchronously, and delivery problems
|
58
|
+
# will result in a bounce to the specified sender.
|
59
|
+
#
|
60
|
+
# Args:
|
61
|
+
# [sender] The From: field of the email. Must correspond to the valid
|
62
|
+
# email address of one of the admins for this application, or
|
63
|
+
# to the email address of the currently logged-in user.
|
64
|
+
# [to] Message recipient(s). Should be an email address, or an Array
|
65
|
+
# of email addresses.
|
66
|
+
# [subject] Subject of the message.
|
67
|
+
# [text] Plain text body of the message. To send an HTML only email,
|
68
|
+
# set +text+ to nil and use the +:html+ option.
|
69
|
+
# [options] See #create_java_message for supported options.
|
70
|
+
def send(sender, to, subject, text, options={})
|
71
|
+
orig_options = options
|
72
|
+
options = {
|
73
|
+
:sender => sender,
|
74
|
+
:to => to || [],
|
75
|
+
:subject => subject,
|
76
|
+
:text => text
|
77
|
+
}
|
78
|
+
options.merge!(orig_options)
|
79
|
+
message = create_java_message(options)
|
80
|
+
convert_mail_exceptions { service.send(message) }
|
81
|
+
end
|
82
|
+
|
83
|
+
# Sends an email alert to all admins of an application.
|
84
|
+
#
|
85
|
+
# The message will be delivered asynchronously, and delivery problems
|
86
|
+
# will result in a bounce to the admins.
|
87
|
+
#
|
88
|
+
# Args:
|
89
|
+
# [sender] The From: field of the email. Must correspond to the valid
|
90
|
+
# email address of one of the admins for this application, or
|
91
|
+
# to the email address of the currently logged-in user.
|
92
|
+
# [subject] Subject of the message.
|
93
|
+
# [text] Plain text body of the message. To send an HTML only email,
|
94
|
+
# set +text+ to nil and use the +:html+ option.
|
95
|
+
# [options] See #create_java_message for supported options.
|
96
|
+
def send_to_admins(sender, subject, text, options={})
|
97
|
+
orig_options = options
|
98
|
+
options = {
|
99
|
+
:sender => sender,
|
100
|
+
:subject => subject,
|
101
|
+
:text => text
|
102
|
+
}
|
103
|
+
options.merge!(orig_options)
|
104
|
+
message = create_java_message(options)
|
105
|
+
convert_mail_exceptions { service.send_to_admins(message) }
|
106
|
+
end
|
107
|
+
|
108
|
+
# Creates a Java MailService.Message object.
|
109
|
+
#
|
110
|
+
# Supported options:
|
111
|
+
# [:atttachments]
|
112
|
+
# Attachments to send with this message. Should be a Hash of
|
113
|
+
# {"filename" => "data"} or an Array of [["filename", "data"], ...].
|
114
|
+
# [:bcc] Must be a String or an Array of Strings if set.
|
115
|
+
# [:cc] Must be a String or an Array of Strings if set.
|
116
|
+
# [:html] The html body of the message. Must not be +nil+ if +text+ is nil.
|
117
|
+
# [:reply_to] Must be a valid email address if set.
|
118
|
+
def create_java_message(options)
|
119
|
+
options[:text_body] = options.delete(:text)
|
120
|
+
options[:html_body] = options.delete(:html)
|
121
|
+
attachments = options[:attachments]
|
122
|
+
if attachments
|
123
|
+
options[:attachments] = attachments.collect do |filename, data|
|
124
|
+
MailService::Attachment.new(filename, data.to_java_bytes)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
[:to, :cc, :bcc].each do |key|
|
128
|
+
value = options[key]
|
129
|
+
options[key] = [value] if value.kind_of? String
|
130
|
+
end
|
131
|
+
message = MailService::Message.new
|
132
|
+
options.each do |key, value|
|
133
|
+
begin
|
134
|
+
message.send("set_#{key}", value) if value
|
135
|
+
rescue NameError
|
136
|
+
raise ArgumentError, "Invalid option #{key.inspect}."
|
137
|
+
end
|
138
|
+
end
|
139
|
+
return message
|
140
|
+
end
|
141
|
+
|
142
|
+
def convert_mail_exceptions # :nodoc:
|
143
|
+
begin
|
144
|
+
yield
|
145
|
+
rescue java.lang.IllegalArgumentException => ex
|
146
|
+
raise ArgumentError, ex.message
|
147
|
+
rescue java.io.IOException => ex
|
148
|
+
raise IOError, ex.message
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def service # :nodoc:
|
153
|
+
@service ||= MailServiceFactory.mail_service
|
154
|
+
end
|
155
|
+
|
156
|
+
def service=(service) # :nodoc:
|
157
|
+
@service = service
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,580 @@
|
|
1
|
+
#!/usr/bin/ruby1.8 -w
|
2
|
+
#
|
3
|
+
# Copyright:: Copyright 2009 Google Inc.
|
4
|
+
# Original Author:: Ryan Brown (mailto:ribrdb@google.com)
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require 'appengine-apis/datastore_types'
|
20
|
+
|
21
|
+
module AppEngine
|
22
|
+
|
23
|
+
# The Ruby API for the App Engine Memcache service. This offers a fast
|
24
|
+
# distrubted cache for commonly-used data. The cache is limited both in
|
25
|
+
# duration and also in total space, so objects stored in it may be discarded
|
26
|
+
# at any time.
|
27
|
+
#
|
28
|
+
# Note that null is a legal value to store in the cache, or to use as a cache
|
29
|
+
# key. Strings are stored encoded as utf-8. To store binary data use
|
30
|
+
# AppEngine::Datastore::Blob or +str.to_java_bytes+.
|
31
|
+
#
|
32
|
+
# The values returned from this API are mutable copies from the cache;
|
33
|
+
# altering them has no effect upon the cached value itself until assigned
|
34
|
+
# with one of the put methods. Likewise, the methods returning collections
|
35
|
+
# return mutable collections, but changes do not affect the cache.
|
36
|
+
#
|
37
|
+
# Except for the #incr and #decr methods, this service does not offer
|
38
|
+
# atomicity guarantees. In particular, operations accessing multiple
|
39
|
+
# keys are non-atomic.
|
40
|
+
#
|
41
|
+
# Increment has a number of caveats to its use; please consult the method
|
42
|
+
# documentation.
|
43
|
+
class Memcache
|
44
|
+
import com.google.appengine.api.memcache.Expiration
|
45
|
+
import com.google.appengine.api.memcache.InvalidValueException
|
46
|
+
import com.google.appengine.api.memcache.LogAndContinueErrorHandler
|
47
|
+
import com.google.appengine.api.memcache.MemcacheServiceException
|
48
|
+
import com.google.appengine.api.memcache.MemcacheServiceFactory
|
49
|
+
import com.google.appengine.api.memcache.MemcacheService
|
50
|
+
import com.google.appengine.api.memcache.StrictErrorHandler
|
51
|
+
|
52
|
+
ADD = MemcacheService::SetPolicy::ADD_ONLY_IF_NOT_PRESENT
|
53
|
+
REPLACE = MemcacheService::SetPolicy::REPLACE_ONLY_IF_PRESENT
|
54
|
+
SET = MemcacheService::SetPolicy::SET_ALWAYS
|
55
|
+
|
56
|
+
# Base Memcache exception class
|
57
|
+
class MemcacheError < StandardError; end
|
58
|
+
|
59
|
+
MemCacheError = MemcacheError
|
60
|
+
ClientError = MemcacheError
|
61
|
+
InternalError = MemcacheError
|
62
|
+
|
63
|
+
MARSHAL_MARKER = '--JRuby Marshal Data--'
|
64
|
+
|
65
|
+
# An exception for backend non-availability or similar error states which
|
66
|
+
# may occur, but are not necessarily indicative of a coding or usage error
|
67
|
+
# by the application.
|
68
|
+
class ServerError < MemcacheError; end
|
69
|
+
|
70
|
+
# Raised when a cache entry has content, but it cannot be read. For example:
|
71
|
+
# - An attempt to #incr a non-integral value
|
72
|
+
# - Version skew between your application and the data in the cache,
|
73
|
+
# causing a marshaling error.
|
74
|
+
class InvalidValueError < MemcacheError; end
|
75
|
+
|
76
|
+
def initialize(*servers)
|
77
|
+
options = if servers[-1].kind_of? Hash
|
78
|
+
servers[-1]
|
79
|
+
else
|
80
|
+
{}
|
81
|
+
end
|
82
|
+
if options.include?(:namespace)
|
83
|
+
service.namespace = options[:namespace]
|
84
|
+
end
|
85
|
+
@readonly = options[:readonly]
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns the Java MemcacheService object used by this Memcache client.
|
89
|
+
def service
|
90
|
+
@service ||= MemcacheServiceFactory.memcache_service
|
91
|
+
end
|
92
|
+
|
93
|
+
def active?
|
94
|
+
# TODO use the capability api to see if Memcache is disabled.
|
95
|
+
true
|
96
|
+
end
|
97
|
+
|
98
|
+
# Empties the cache of all values. Statistics are not affected. Note that
|
99
|
+
# #clear does not respect namespaces - this flushes the cache for every
|
100
|
+
# namespace.
|
101
|
+
#
|
102
|
+
# Returns true on success, false on RPC or server error.
|
103
|
+
def flush_all
|
104
|
+
check_write
|
105
|
+
with_errors do
|
106
|
+
begin
|
107
|
+
service.clear_all
|
108
|
+
return true
|
109
|
+
rescue MemcacheError
|
110
|
+
return false
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
alias clear flush_all
|
115
|
+
|
116
|
+
# Gets memcache statistics for this application.
|
117
|
+
#
|
118
|
+
# All of these statistics may reset due to various transient conditions.
|
119
|
+
# They provide the best information available at the time of being called.
|
120
|
+
#
|
121
|
+
# Returns a Hash mapping statistic names to associated values:
|
122
|
+
# [:hits] Number of cache get requests resulting in a cache hit.
|
123
|
+
# [:misses] Number of cache get requests resulting in a cache miss.
|
124
|
+
# [:byte_hits] Sum of bytes transferred on get requests. Rolls over to
|
125
|
+
# zero on overflow.
|
126
|
+
# [:items] Number of key/value pairs in the cache.
|
127
|
+
# [:bytes] Total size of all items in the cache.
|
128
|
+
# [:oldest_item_age]
|
129
|
+
# How long in seconds since the oldest item in the
|
130
|
+
# cache was accessed. Effectively, this indicates how long a new
|
131
|
+
# item will survive in the cache without being accessed. This is
|
132
|
+
# _not_ the amount of time that has elapsed since the item was
|
133
|
+
# created.
|
134
|
+
#
|
135
|
+
# On error, returns +nil+.
|
136
|
+
def stats
|
137
|
+
with_errors do
|
138
|
+
begin
|
139
|
+
stats = service.statistics
|
140
|
+
if stats
|
141
|
+
{
|
142
|
+
:hits => stats.hit_count,
|
143
|
+
:misses => stats.miss_count,
|
144
|
+
:byte_hits => stats.bytes_returned_for_hits,
|
145
|
+
:items => stats.item_count,
|
146
|
+
:bytes => stats.total_item_bytes,
|
147
|
+
:oldest_item_age => stats.max_time_without_access / 1000.0
|
148
|
+
}
|
149
|
+
end
|
150
|
+
rescue ServerError
|
151
|
+
nil
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Fetch and return the values associated with the given +key+s from the
|
157
|
+
# cache. Returns +nil+ for any value that wasn’t in the cache.
|
158
|
+
def get(*keys)
|
159
|
+
multiple = (keys.size != 1)
|
160
|
+
if !multiple && keys[0].kind_of?(Array)
|
161
|
+
keys = keys[0]
|
162
|
+
multiple = true
|
163
|
+
end
|
164
|
+
hash = get_hash(*keys)
|
165
|
+
values = keys.collect {|key| hash[key]}
|
166
|
+
if multiple
|
167
|
+
values
|
168
|
+
else
|
169
|
+
values[0]
|
170
|
+
end
|
171
|
+
end
|
172
|
+
alias [] get
|
173
|
+
|
174
|
+
# Looks up multiple keys from memcache in one operation. This is more
|
175
|
+
# efficient than multiple separate calls to #get.
|
176
|
+
#
|
177
|
+
# Args:
|
178
|
+
# - keys: List of keys to look up.
|
179
|
+
#
|
180
|
+
# Returns a hash of the keys and values that were present in memcache.
|
181
|
+
def get_hash(*keys)
|
182
|
+
key_map = KeyMap.new(keys)
|
183
|
+
convert_exceptions do
|
184
|
+
map = service.getAll(key_map.java_keys)
|
185
|
+
key_map.map_to_hash(map) do |value|
|
186
|
+
if value.java_kind_of?(java.util.ArrayList) && value.size == 2 &&
|
187
|
+
value[0] == MARSHAL_MARKER
|
188
|
+
Marshal.load(String.from_java_bytes(value[1]))
|
189
|
+
else
|
190
|
+
value
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Removes the given key from the cache, and prevents it from being added
|
197
|
+
# using #add for +time+ seconds thereafter. Calls to #set are not blocked.
|
198
|
+
#
|
199
|
+
# Returns true if an entry existed to delete.
|
200
|
+
def delete(key, time=nil)
|
201
|
+
time ||= 0
|
202
|
+
check_write
|
203
|
+
convert_exceptions do
|
204
|
+
service.delete(memcache_key(key), time * 1000)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Removes the given keys from the cache, and prevents them from being added
|
209
|
+
# using #add for +time+ seconds thereafter. Calls to #set are not blocked.
|
210
|
+
#
|
211
|
+
# Returns the set of keys deleted. Any keys in +keys+ but not in the
|
212
|
+
# returned set were not found in the cache.
|
213
|
+
def delete_many(keys, time=0)
|
214
|
+
check_write
|
215
|
+
key_map = KeyMap.new(keys)
|
216
|
+
convert_exceptions do
|
217
|
+
java_keys = service.delete_all(key_map.java_keys, time * 1000)
|
218
|
+
key_map.ruby_keys(java_keys)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# Sets a key's value, iff item is not already in memcache.
|
223
|
+
#
|
224
|
+
# Args:
|
225
|
+
# - key: Key to set.
|
226
|
+
# - value: Value to set. Any type. If complex, will be marshaled.
|
227
|
+
# - expiration: Optional expiration time, either relative number of seconds
|
228
|
+
# from current time (up to 1 month), an absolute Unix epoch time, or a
|
229
|
+
# Time. By default, items never expire, though items may be evicted due
|
230
|
+
# to memory pressure.
|
231
|
+
#
|
232
|
+
# Returns true if added, false on error.
|
233
|
+
def add(key, value, expiration=0)
|
234
|
+
put(key, value, expiration, ADD)
|
235
|
+
end
|
236
|
+
|
237
|
+
# Set multiple keys' values iff items are not already in memcache.
|
238
|
+
#
|
239
|
+
# Args:
|
240
|
+
# - pairs: Hash of keys to values, or Array of [key, value] pairs.
|
241
|
+
# - expiration: Optional expiration time, either relative number of seconds
|
242
|
+
# from current time (up to 1 month), an absolute Unix epoch time, or a
|
243
|
+
# Time. By default, items never expire, though items may be evicted due
|
244
|
+
# to memory pressure.
|
245
|
+
#
|
246
|
+
# Returns a list of keys whose values were NOT set. On total success
|
247
|
+
# this list should be empty.
|
248
|
+
def add_many(pairs, expiration=0)
|
249
|
+
put_many(pairs, expiration, ADD)
|
250
|
+
end
|
251
|
+
|
252
|
+
# Sets a key's value, regardless of previous contents in cache.
|
253
|
+
#
|
254
|
+
# Unlike #add and #replace, this method always sets (or
|
255
|
+
# overwrites) the value in memcache, regardless of previous
|
256
|
+
# contents.
|
257
|
+
#
|
258
|
+
# Args:
|
259
|
+
# - key: Key to set.
|
260
|
+
# - value: Value to set. Any type. If complex, will be marshaled.
|
261
|
+
# - expiration: Optional expiration time, either relative number of seconds
|
262
|
+
# from current time (up to 1 month), an absolute Unix epoch time, or a
|
263
|
+
# Time. By default, items never expire, though items may be evicted due
|
264
|
+
# to memory pressure.
|
265
|
+
#
|
266
|
+
# Returns true if set, false on error.
|
267
|
+
def set(key, value, expiration=0)
|
268
|
+
put(key, value, expiration, SET)
|
269
|
+
end
|
270
|
+
|
271
|
+
# Set multiple keys' values at once, regardless of previous contents.
|
272
|
+
#
|
273
|
+
# Args:
|
274
|
+
# - pairs: Hash of keys to values, or Array of [key, value] pairs.
|
275
|
+
# - expiration: Optional expiration time, either relative number of seconds
|
276
|
+
# from current time (up to 1 month), an absolute Unix epoch time, or a
|
277
|
+
# Time. By default, items never expire, though items may be evicted due
|
278
|
+
# to memory pressure.
|
279
|
+
#
|
280
|
+
# Returns a list of keys whose values were NOT set. On total success
|
281
|
+
# this list should be empty.
|
282
|
+
def set_many(pairs, expiration=0)
|
283
|
+
put_many(pairs, expiration, SET)
|
284
|
+
end
|
285
|
+
|
286
|
+
# call-seq:
|
287
|
+
# cache[:foo, :bar] = 1, 2
|
288
|
+
#
|
289
|
+
# Syntactic sugar for calling set_many.
|
290
|
+
def []=(*args)
|
291
|
+
values = args.pop
|
292
|
+
if values.kind_of? Array
|
293
|
+
set_many(args.zip(values))
|
294
|
+
else
|
295
|
+
set(args, values)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
# Replaces a key's value, failing if item isn't already in memcache.
|
300
|
+
#
|
301
|
+
# Unlike #add and #replace, this method always sets (or
|
302
|
+
# overwrites) the value in memcache, regardless of previous
|
303
|
+
# contents.
|
304
|
+
#
|
305
|
+
# Args:
|
306
|
+
# - key: Key to set.
|
307
|
+
# - value: Value to set. Any type. If complex, will be marshaled.
|
308
|
+
# - expiration: Optional expiration time, either relative number of seconds
|
309
|
+
# from current time (up to 1 month), an absolute Unix epoch time, or a
|
310
|
+
# Time. By default, items never expire, though items may be evicted due
|
311
|
+
# to memory pressure.
|
312
|
+
#
|
313
|
+
# Returns true if replaced, false on cache miss.
|
314
|
+
def replace(key, value, expiration=0)
|
315
|
+
put(key, value, expiration, REPLACE)
|
316
|
+
end
|
317
|
+
|
318
|
+
# Replace multiple keys' values, failing if the items aren't in memcache.
|
319
|
+
#
|
320
|
+
# Args:
|
321
|
+
# - pairs: Hash of keys to values, or Array of [key, value] pairs.
|
322
|
+
# - expiration: Optional expiration time, either relative number of seconds
|
323
|
+
# from current time (up to 1 month), an absolute Unix epoch time, or a
|
324
|
+
# Time. By default, items never expire, though items may be evicted due
|
325
|
+
# to memory pressure.
|
326
|
+
#
|
327
|
+
# Returns a list of keys whose values were NOT set. On total success
|
328
|
+
# this list should be empty.
|
329
|
+
def replace_many(pairs, expiration=0)
|
330
|
+
put_many(pairs, expiration, REPLACE)
|
331
|
+
end
|
332
|
+
|
333
|
+
# Atomically fetches, increments, and stores a given integral value.
|
334
|
+
# "Integral" types are Fixnum and in some cases String (if the string is
|
335
|
+
# parseable as a number. The entry must already exist.
|
336
|
+
#
|
337
|
+
# Internally, the value is a unsigned 64-bit integer. Memcache
|
338
|
+
# doesn't check 64-bit overflows. The value, if too large, will
|
339
|
+
# wrap around.
|
340
|
+
#
|
341
|
+
# Args:
|
342
|
+
# - key: the key of the entry to manipulate
|
343
|
+
# - delta: the size of the increment.
|
344
|
+
#
|
345
|
+
# Returns the post-increment value.
|
346
|
+
#
|
347
|
+
# Throws InvalidValueError if the object incremented is not of
|
348
|
+
# an integral type.
|
349
|
+
def incr(key, delta=1)
|
350
|
+
check_write
|
351
|
+
convert_exceptions do
|
352
|
+
service.increment(memcache_key(key), delta)
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# Atomically fetches, deccrements, and stores a given integral value.
|
357
|
+
# "Integral" types are Fixnum and in some cases String (if the string is
|
358
|
+
# parseable as a number. The entry must already exist.
|
359
|
+
#
|
360
|
+
# Internally, the value is a unsigned 64-bit integer. Memcache
|
361
|
+
# caps decrementing below zero to zero.
|
362
|
+
#
|
363
|
+
# Args:
|
364
|
+
# - key: the key of the entry to manipulate
|
365
|
+
# - delta: the size of the decrement
|
366
|
+
#
|
367
|
+
# Returns the post-decrement value.
|
368
|
+
#
|
369
|
+
# Throws InvalidValueError if the object decremented is not of
|
370
|
+
# an integral type.
|
371
|
+
def decr(key, delta=1)
|
372
|
+
check_write
|
373
|
+
convert_exceptions do
|
374
|
+
service.increment(memcache_key(key), -delta)
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
# Get the name of the namespace that will be used in API calls.
|
379
|
+
def namespace
|
380
|
+
service.namespace
|
381
|
+
end
|
382
|
+
|
383
|
+
# Change the namespace used in API calls.
|
384
|
+
def namespace=(value)
|
385
|
+
service.namespace = value
|
386
|
+
end
|
387
|
+
|
388
|
+
# Returns true if the cache was created read-only.
|
389
|
+
def readonly?
|
390
|
+
@readonly
|
391
|
+
end
|
392
|
+
|
393
|
+
def inspect
|
394
|
+
"<Memcache ns:#{namespace.inspect}, ro:#{readonly?.inspect}>"
|
395
|
+
end
|
396
|
+
|
397
|
+
# Returns whether the client raises an exception if there's an error
|
398
|
+
# contacting the server. By default it will simulate a cache miss
|
399
|
+
# instead of raising an error.
|
400
|
+
def raise_errors?
|
401
|
+
service.error_handler.kind_of? StrictErrorHandler
|
402
|
+
end
|
403
|
+
|
404
|
+
# Set whether this client raises an exception if there's an error
|
405
|
+
# contacting the server.
|
406
|
+
#
|
407
|
+
# If +should_raise+ is true, a ServerError is raised whenever there
|
408
|
+
# is an error contacting the server.
|
409
|
+
#
|
410
|
+
# If +should_raise+ is false (the default), a cache miss is simulated
|
411
|
+
# instead of raising an error.
|
412
|
+
def raise_errors=(should_raise)
|
413
|
+
if should_raise
|
414
|
+
service.error_handler = StrictErrorHandler.new
|
415
|
+
else
|
416
|
+
service.error_handler = LogAndContinueErrorHandler.new
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
# For backwards compatibility. Simply returns nil
|
421
|
+
def do_nothing(*args)
|
422
|
+
end
|
423
|
+
alias server_item_stats do_nothing
|
424
|
+
alias server_malloc_stats do_nothing
|
425
|
+
alias server_map_stats do_nothing
|
426
|
+
alias server_reset_stats do_nothing
|
427
|
+
alias server_size_stats do_nothing
|
428
|
+
alias server_slab_stats do_nothing
|
429
|
+
alias server_stats do_nothing
|
430
|
+
alias servers= do_nothing
|
431
|
+
|
432
|
+
private
|
433
|
+
|
434
|
+
def memcache_key(obj)
|
435
|
+
key = obj
|
436
|
+
key = key.to_s.to_java_string if key
|
437
|
+
key
|
438
|
+
end
|
439
|
+
|
440
|
+
def memcache_value(obj)
|
441
|
+
case obj
|
442
|
+
when Fixnum
|
443
|
+
java.lang.Long.new(obj)
|
444
|
+
when Float
|
445
|
+
java.lang.Double.new(obj)
|
446
|
+
when TrueClass, FalseClass
|
447
|
+
java.lang.Boolean.new(obj)
|
448
|
+
when JavaProxy, Java::JavaObject
|
449
|
+
obj
|
450
|
+
else
|
451
|
+
if obj.class == String
|
452
|
+
# Convert plain strings to Java strings
|
453
|
+
obj.to_java_string
|
454
|
+
else
|
455
|
+
bytes = Marshal.dump(obj).to_java_bytes
|
456
|
+
java.util.ArrayList.new([MARSHAL_MARKER.to_java_string, bytes])
|
457
|
+
end
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
def memcache_expiration(amount)
|
462
|
+
if amount.nil? || amount == 0
|
463
|
+
nil
|
464
|
+
elsif amount.kind_of? Time
|
465
|
+
Expiration.on_date(amount.to_java)
|
466
|
+
elsif amount > 86400 * 30
|
467
|
+
millis = (amount * 1000).to_i
|
468
|
+
Expiration.on_date(java.util.Date.new(millis))
|
469
|
+
else
|
470
|
+
Expiration.byDeltaMillis((amount * 1000).to_i)
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
def check_write
|
475
|
+
raise MemcacheError, "readonly cache" if self.readonly?
|
476
|
+
end
|
477
|
+
|
478
|
+
def with_errors(&block)
|
479
|
+
saved_handler = service.error_handler
|
480
|
+
begin
|
481
|
+
service.error_handler = StrictErrorHandler.new
|
482
|
+
convert_exceptions(&block)
|
483
|
+
ensure
|
484
|
+
service.error_handler = saved_handler
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
def convert_exceptions
|
489
|
+
begin
|
490
|
+
yield
|
491
|
+
rescue java.lang.IllegalArgumentException => ex
|
492
|
+
raise ArgumentError, ex.message
|
493
|
+
rescue InvalidValueException => ex
|
494
|
+
raise InvalidValueError, ex.message
|
495
|
+
rescue MemcacheServiceException => ex
|
496
|
+
raise ServerError, ex.message
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
def put(key, value, expiration, mode)
|
501
|
+
check_write
|
502
|
+
convert_exceptions do
|
503
|
+
key = memcache_key(key)
|
504
|
+
value = memcache_value(value)
|
505
|
+
expiriation = memcache_expiration(expiriation)
|
506
|
+
service.put(key, value, expiriation, mode)
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
def put_many(pairs, expiration, mode)
|
511
|
+
check_write
|
512
|
+
expiration = memcache_expiration(expiration)
|
513
|
+
convert_exceptions do
|
514
|
+
key_map = KeyMap.new
|
515
|
+
put_map = java.util.HashMap.new
|
516
|
+
pairs.each do |key, value|
|
517
|
+
java_key = key_map << key
|
518
|
+
java_value = memcache_value(value)
|
519
|
+
put_map.put(java_key, java_value)
|
520
|
+
end
|
521
|
+
saved_keys = service.put_all(put_map, expiration, mode)
|
522
|
+
key_map.missing_keys(saved_keys)
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
class KeyMap # :nodoc:
|
527
|
+
def initialize(keys=[])
|
528
|
+
@orig_keys = []
|
529
|
+
@map = {}
|
530
|
+
keys.each do |key|
|
531
|
+
self << key
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
def <<(key)
|
536
|
+
@orig_keys << key
|
537
|
+
string_key = if key
|
538
|
+
key.to_s
|
539
|
+
else
|
540
|
+
key
|
541
|
+
end
|
542
|
+
@map[string_key] = key
|
543
|
+
if string_key
|
544
|
+
string_key.to_java_string
|
545
|
+
else
|
546
|
+
string_key
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
def java_keys
|
551
|
+
@map.keys.collect do |key|
|
552
|
+
if key
|
553
|
+
key.to_java_string
|
554
|
+
else
|
555
|
+
key
|
556
|
+
end
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
def ruby_keys(keys)
|
561
|
+
keys.collect {|key| @map[key]}
|
562
|
+
end
|
563
|
+
|
564
|
+
def missing_keys(keys)
|
565
|
+
@orig_keys - ruby_keys(keys)
|
566
|
+
end
|
567
|
+
|
568
|
+
def map_to_hash(java_map)
|
569
|
+
hash = {}
|
570
|
+
if java_map
|
571
|
+
java_map.each do |key, value|
|
572
|
+
value = yield(value)
|
573
|
+
hash[@map[key]] = value
|
574
|
+
end
|
575
|
+
end
|
576
|
+
hash
|
577
|
+
end
|
578
|
+
end
|
579
|
+
end
|
580
|
+
end
|