Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6dfc849

Browse files
engelkeAce NassriTakashi Matsuo
authored
feat: add GAE memorystore migration sample (GoogleCloudPlatform#4427)
Co-authored-by: Ace Nassri <[email protected]> Co-authored-by: Takashi Matsuo <[email protected]>
1 parent 3454865 commit 6dfc849

File tree

11 files changed

+348
-0
lines changed

11 files changed

+348
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# This file specifies files that are *not* uploaded to Google Cloud Platform
2+
# using gcloud. It follows the same syntax as .gitignore, with the addition of
3+
# "#!include" directives (which insert the entries of the given .gitignore-style
4+
# file at that point).
5+
#
6+
# For more information, run:
7+
# $ gcloud topic gcloudignore
8+
#
9+
.gcloudignore
10+
# If you would like to upload your .git directory, .gitignore file or files
11+
# from your .gitignore file, remove the corresponding line
12+
# below:
13+
.git
14+
.gitignore
15+
16+
# Python pycache:
17+
__pycache__/
18+
# Ignored by the build system
19+
/setup.cfg
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
## App Engine Memorystore for Redis Sample
2+
3+
[![Open in Cloud Shell][shell_img]][shell_link]
4+
5+
[shell_img]: http://gstatic.com/cloudssh/images/open-btn.png
6+
[shell_link]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=appengine/standard/migration/memorystore/README.md
7+
8+
This is a sample app for Google App Engine that demonstrates how to replace
9+
use of the [Memcache API](https://cloud.google.com/appengine/docs/standard/python/memcache)
10+
with the [Memorystore for Redis offering](https://cloud.google.com/memorystore).
11+
This newer library can be used on App Engine with either Python 2.7
12+
or Python 3.
13+
14+
Code taken from the [Memcache sample](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/master/appengine/standard/memcache/snippets/snippets.py)
15+
is included in this new sample, with each line commented out with two # marks.
16+
This allows the older solution's approach to be contrasted with the
17+
Memorystore for Redis approach.
18+
19+
Prior to deploying this sample, a
20+
[serverless VPC connector](https://cloud.google.com/vpc/docs/configure-serverless-vpc-access)
21+
must be created and then a
22+
[Memorystore for Redis instance](https://cloud.google.com/memorystore/docs/redis/quickstart-console)
23+
on the same VPC. The IP address and port number of the Redis instance, and
24+
the name of the VPC connector should be entered in either app.yaml
25+
(for Python 2.7) or app3.yaml (for Python 3).
26+
27+
To deploy and run this sample in App Engine standard for Python 2.7:
28+
29+
pip install -t lib -r requirements.txt
30+
gcloud app deploy
31+
32+
To deploy and run this sample in App Engine standard for Python 3.7:
33+
34+
gcloud app deploy app3.yaml
35+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
runtime: python27
2+
api_version: 1
3+
threadsafe: yes
4+
5+
handlers:
6+
- url: .*
7+
script: main.app
8+
9+
libraries:
10+
- name: setuptools
11+
version: 36.6.0
12+
- name: grpcio
13+
version: 1.0.0
14+
15+
env_variables:
16+
REDISHOST: '<REDIS_HOST>'
17+
REDISPORT: '<REDIS_PORT>'
18+
19+
vpc_access_connector:
20+
name: 'projects/<PROJECT_ID>/locations/<REGION>/connectors/<CONNECTOR_NAME>'
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
runtime: python38
2+
3+
env_variables:
4+
REDISHOST: '<REDIS_HOST>'
5+
REDISPORT: '<REDIS_PORT>'
6+
7+
vpc_access_connector:
8+
name: 'projects/<PROJECT_ID>/locations/<REGION>/connectors/<CONNECTOR_NAME>'
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import pkg_resources
2+
from google.appengine.ext import vendor
3+
4+
# Set path to your libraries folder.
5+
path = 'lib'
6+
# Add libraries installed in the path folder.
7+
vendor.add(path)
8+
# Add libraries to pkg_resources working set to find the distribution.
9+
pkg_resources.working_set.add_entry(path)
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Copyright 2020 Google LLC
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+
# This code contrasts how Python 2.7 apps using the memcache library can be
16+
# changed to use the Memorycache for Redis API and libraries, instead, under
17+
# either Python 2.7 or Python 3.6 or later.
18+
#
19+
# The examples are taken from the appengine/standard/memcache/snippets example
20+
# in this repository. The lines of code from the old example are included here
21+
# as comments, preceded by ## to distinguish them.
22+
#
23+
# The samples are wrapped in a Flask web app to run in App Engine.
24+
25+
## from google.appengine.api import memcache
26+
import os
27+
import redis
28+
import time
29+
30+
from flask import Flask, redirect, render_template, request
31+
32+
redis_host = os.environ.get('REDIS_HOST', 'localhost')
33+
redis_port = os.environ.get('REDIS_PORT', '6379')
34+
client = redis.Redis(host=redis_host, port=redis_port)
35+
36+
37+
# Dummy store that this sample shows being cached
38+
def query_for_data():
39+
# Add artificial delay so user can see when cache has been used
40+
time.sleep(5)
41+
return 'prestored value'
42+
43+
44+
def get_data(cache_key):
45+
## data = memcache.get('key')
46+
data = client.get(cache_key)
47+
48+
if data is not None:
49+
return data.decode()
50+
else:
51+
data = query_for_data()
52+
## memcache.add('key', data, 60)
53+
client.set(cache_key, data, ex=60)
54+
55+
return data
56+
57+
58+
def add_values(values, expires=3600):
59+
# Add a value if it doesn't exist in the cache
60+
# with a cache expiration of 1 hour.
61+
## memcache.add(key="weather_USA_98105", value="raining", time=3600)
62+
## # Remove one of the values and set by itself first
63+
## first_key = values.keys()[0]
64+
## first_value = values[first_key]
65+
## del values[first_key]
66+
## memcache.add(first_key, first_value, ex=expires)
67+
##
68+
## # Set several values, overwriting any existing values for these keys.
69+
## memcache.set_multi(
70+
## {"USA_98115": "cloudy", "USA_94105": "foggy", "USA_94043": "sunny"},
71+
## key_prefix="weather_",
72+
## time=3600
73+
## )
74+
# Redis mset is similar to memcache.set_multi, but cannot set expirations
75+
client.mset(values)
76+
77+
# Rather than set expiration with separate operations for each key, batch
78+
# them using pipeline
79+
with client.pipeline() as pipe:
80+
for name in values:
81+
pipe.pexpire(name, expires * 1000) # Time in milliseconds
82+
pipe.execute()
83+
84+
85+
def increment_counter(name, expires=60, value=0):
86+
# Atomically increment an integer value.
87+
## memcache.set(key="counter", value=0)
88+
client.set(name, value, ex=expires)
89+
## memcache.incr("counter")
90+
client.incr(name)
91+
## memcache.incr("counter")
92+
client.incr(name)
93+
## memcache.incr("counter")
94+
client.incr(name)
95+
96+
97+
# Web app to invoke above samples follows
98+
app = Flask(__name__)
99+
100+
101+
@app.route('/', methods=['GET'])
102+
def home():
103+
return render_template('index.html')
104+
105+
106+
@app.route('/showdata', methods=['GET'])
107+
def showdata():
108+
data = get_data('data')
109+
110+
values = {}
111+
for key in client.keys():
112+
if key == 'data':
113+
next
114+
values[key.decode()] = client.get(key).decode()
115+
116+
return render_template('showdata.html', data=data, values=values)
117+
118+
119+
@app.route('/showdata', methods=['POST'])
120+
def savedata():
121+
key = request.form['key']
122+
value = request.form['value']
123+
add_values({key: value})
124+
if key == 'counter':
125+
increment_counter('counter', expires=60, value=value)
126+
return redirect('/')
127+
128+
129+
if __name__ == '__main__':
130+
# This is used when running locally.
131+
app.run(host='127.0.0.1', port=8080, debug=True)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright 202 Google LLC
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+
from mock import patch
16+
import pytest
17+
import redis
18+
import uuid
19+
20+
import main
21+
22+
KEY_PREFIX = str(uuid.uuid4()) # Avoid interfering with other tests
23+
TEST_VALUES = {
24+
KEY_PREFIX + "weather_USA_98105": "raining",
25+
KEY_PREFIX + "weather_USA_98115": "cloudy",
26+
KEY_PREFIX + "weather_USA_94105": "foggy",
27+
KEY_PREFIX + "weather_USA_94043": "sunny",
28+
}
29+
30+
31+
@patch('main.query_for_data', return_value='data')
32+
def test_get_data_not_present(query_fn, testbed):
33+
try:
34+
main.client.set(KEY_PREFIX + 'counter', '0', 9000)
35+
except redis.RedisError:
36+
pytest.skip('Redis is unavailable')
37+
38+
data = main.get_data(KEY_PREFIX + 'key')
39+
query_fn.assert_called_once_with()
40+
assert data == 'data'
41+
assert 'data' == main.client.get(KEY_PREFIX + 'key')
42+
main.client.delete(KEY_PREFIX + 'key')
43+
44+
45+
@patch('main.query_for_data', return_value='data')
46+
def test_get_data_present(query_fn, testbed):
47+
try:
48+
main.client.set(KEY_PREFIX + 'key', 'data', 9000)
49+
except Exception:
50+
pytest.skip('Redis is unavailable')
51+
52+
data = main.get_data()
53+
query_fn.assert_not_called()
54+
assert data == 'data'
55+
main.client.delete(KEY_PREFIX + 'key')
56+
57+
58+
def test_add_values(testbed):
59+
try:
60+
main.client.set(KEY_PREFIX + 'counter', '0', 9000)
61+
except Exception:
62+
pytest.skip('Redis is unavailable')
63+
64+
main.add_values(TEST_VALUES)
65+
for key, value in TEST_VALUES.iteritems():
66+
assert main.client.get(key) == value
67+
assert main.client.get(KEY_PREFIX + 'counter') == 3
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pytest==4.6.11; python_version < '3.0'
2+
mock==3.0.5
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
redis==3.5.3
2+
flask==1.1.2
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>Memorystore Example</title>
5+
</head>
6+
7+
<body>
8+
<h1>Memorystore Example</h1>
9+
10+
<p>
11+
The <a href="/showdata">following page</a> will display stored
12+
data. The first time it is displayed it will fetch the value of
13+
"data" from a slow store. Future refreshes will fetch it from cache
14+
until it expires (60 seconds in this example).
15+
</p>
16+
17+
<p>
18+
Other values will be fetched only from the cache, if present. Values
19+
can be stored in the cache by entering them in the form.
20+
</p>
21+
</body>
22+
</html >
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>Stored Data</title>
5+
</head>
6+
7+
<body>
8+
<h1>Stored Data</h1>
9+
10+
<p>
11+
The value of "data" is {{ data }}.
12+
</p>
13+
14+
<p>
15+
<ul>
16+
{% for key, value in values.items() %}
17+
<li>The value of "{{ key }}" is "{{ value }}".</li>
18+
{% endfor %}
19+
</ul>
20+
</p>
21+
22+
<form action="/showdata" method="post">
23+
<div>
24+
Add another entry name <input type="text" name="key" id="key" />
25+
with value <input type="text" name="value" id="value" />. Note, any
26+
value named "counter" will be incremented three times after storing.
27+
</div>
28+
<div>
29+
<input type="submit" value="Store New Entry" />
30+
</div>
31+
</form>
32+
</body>
33+
</html >

0 commit comments

Comments
 (0)