appengine-apis 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|