Repository navigation
Expand file tree
/
Copy pathdefault_api_stub.py
More file actions
executable file
·369 lines (257 loc) · 11.1 KB
/
Copy pathdefault_api_stub.py
File metadata and controls
executable file
·369 lines (257 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
#!/usr/bin/env python
#
# Copyright 2007 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""An APIProxy stub that communicates with VMEngine service bridges."""
from concurrent import futures
import logging
import os
import sys
import threading
from google.appengine.api import apiproxy_rpc
from google.appengine.api import apiproxy_stub_map
from google.appengine.ext.remote_api import remote_api_bytes_pb2 as remote_api_pb2
from google.appengine.runtime import apiproxy_errors
from google.appengine.runtime import context
import six.moves.urllib.parse
import urllib3
logging.getLogger('requests_nologs').setLevel(logging.ERROR)
TICKET_HEADER = 'HTTP_X_APPENGINE_API_TICKET'
DEV_TICKET_HEADER = 'HTTP_X_APPENGINE_DEV_REQUEST_ID'
DAPPER_ENV_KEY = 'HTTP_X_GOOGLE_DAPPERTRACEINFO'
SERVICE_BRIDGE_HOST = 'appengine.googleapis.internal'
API_PORT = 10001
SERVICE_ENDPOINT_NAME = 'app-engine-apis'
APIHOST_METHOD = '/VMRemoteAPI.CallRemoteAPI'
PROXY_PATH = '/rpc_http'
DAPPER_HEADER = 'X-Google-DapperTraceInfo'
SERVICE_DEADLINE_HEADER = 'X-Google-RPC-Service-Deadline'
SERVICE_ENDPOINT_HEADER = 'X-Google-RPC-Service-Endpoint'
SERVICE_METHOD_HEADER = 'X-Google-RPC-Service-Method'
RPC_CONTENT_TYPE = 'application/octet-stream'
DEFAULT_TIMEOUT = 60
DEADLINE_DELTA_SECONDS = 1
MAX_CONCURRENT_API_CALLS = 100
URLLIB3_POOL_COUNT = 10
POOL_SIZE_LOWER_LIMIT = 10
POOL_SIZE_UPPER_LIMIT = 100
POOL_SIZE_DEFAULT_VALUE = 10
try:
URLLIB3_POOL_SIZE = int(
os.environ.get('POOL_SIZE_URLLIB3', POOL_SIZE_DEFAULT_VALUE)
)
except (ValueError, TypeError):
URLLIB3_POOL_SIZE = POOL_SIZE_DEFAULT_VALUE
if not (POOL_SIZE_LOWER_LIMIT <= URLLIB3_POOL_SIZE <= POOL_SIZE_UPPER_LIMIT):
URLLIB3_POOL_SIZE = POOL_SIZE_DEFAULT_VALUE
_EXCEPTIONS_MAP = {
remote_api_pb2.RpcError.UNKNOWN:
(apiproxy_errors.RPCFailedError,
'The remote RPC to the application server failed for call %s.%s().'),
remote_api_pb2.RpcError.CALL_NOT_FOUND:
(apiproxy_errors.CallNotFoundError,
'The API package \'%s\' or call \'%s()\' was not found.'),
remote_api_pb2.RpcError.PARSE_ERROR:
(apiproxy_errors.ArgumentError,
'There was an error parsing arguments for API call %s.%s().'),
remote_api_pb2.RpcError.OVER_QUOTA:
(apiproxy_errors.OverQuotaError,
'The API call %s.%s() required more quota than is available.'),
remote_api_pb2.RpcError.REQUEST_TOO_LARGE:
(apiproxy_errors.RequestTooLargeError,
'The request to API call %s.%s() was too large.'),
remote_api_pb2.RpcError.CAPABILITY_DISABLED:
(apiproxy_errors.CapabilityDisabledError,
'The API call %s.%s() is temporarily disabled.'),
remote_api_pb2.RpcError.FEATURE_DISABLED:
(apiproxy_errors.FeatureNotEnabledError,
'The API call %s.%s() is currently not enabled.'),
remote_api_pb2.RpcError.RESPONSE_TOO_LARGE:
(apiproxy_errors.ResponseTooLargeError,
'The response from API call %s.%s() was too large.'),
remote_api_pb2.RpcError.CANCELLED:
(apiproxy_errors.CancelledError,
'The API call %s.%s() was explicitly cancelled.'),
remote_api_pb2.RpcError.DEADLINE_EXCEEDED:
(apiproxy_errors.DeadlineExceededError,
'The API call %s.%s() took too long to respond and was cancelled.')
}
_DEFAULT_EXCEPTION = _EXCEPTIONS_MAP[remote_api_pb2.RpcError.UNKNOWN]
_DEADLINE_EXCEEDED_EXCEPTION = _EXCEPTIONS_MAP[
remote_api_pb2.RpcError.DEADLINE_EXCEEDED]
class DefaultApiRPC(apiproxy_rpc.RPC):
"""A class representing an RPC to a remote server."""
def _ErrorException(self, exception_class, error_details):
return exception_class(error_details % (self.package, self.call))
def _TranslateToError(self, response):
"""Translates a failed APIResponse into an exception."""
if response.HasField('rpc_error'):
code = response.rpc_error.code
detail = response.rpc_error.detail
exception_type, msg = _EXCEPTIONS_MAP.get(code, _DEFAULT_EXCEPTION)
if detail:
msg = '%s -- Additional details from server: %s' % (msg, detail)
raise self._ErrorException(exception_type, msg)
raise apiproxy_errors.ApplicationError(response.application_error.code,
response.application_error.detail)
def _MakeCallImpl(self):
"""Makes an asynchronous API call over the service bridge.
For this to work the following must be set:
self.package: the API package name;
self.call: the name of the API call/method to invoke;
self.request: the API request body as a serialized protocol buffer.
The actual API call is made by urllib3.request via a thread pool
(multiprocessing.dummy.Pool). The thread pool restricts the number of
concurrent requests to MAX_CONCURRENT_API_CALLS, so this method will
block if that limit is exceeded, until other asynchronous calls resolve.
If the main thread holds the import lock, waiting on thread work can cause
a deadlock:
https://docs.python.org/2/library/threading.html#importing-in-threaded-code
Therefore, we try to detect this error case and fall back to sync calls.
"""
assert self._state == apiproxy_rpc.RPC.IDLE, self._state
ticket = None
if DefaultApiStub.ShouldUseRequestSecurityTicketForThread():
if context.READ_FROM_OS_ENVIRON:
ticket = os.environ.get(TICKET_HEADER,
os.environ.get(DEV_TICKET_HEADER))
else:
ticket = context.gae_headers.API_TICKET.get(
context.gae_headers.DEV_REQUEST_ID.get(None))
if not ticket:
raise apiproxy_errors.RPCFailedError(
'Attempted RPC call without active security ticket')
request = remote_api_pb2.Request(
service_name=self.package,
method=self.call,
request_id=ticket,
request=self.request.SerializeToString())
deadline = self.deadline or DEFAULT_TIMEOUT
body_data = request.SerializeToString()
headers = {
SERVICE_DEADLINE_HEADER: str(deadline),
SERVICE_ENDPOINT_HEADER: SERVICE_ENDPOINT_NAME,
SERVICE_METHOD_HEADER: APIHOST_METHOD,
'Content-type': RPC_CONTENT_TYPE,
}
dapper_header_value = context.get(DAPPER_ENV_KEY)
if dapper_header_value:
headers[DAPPER_HEADER] = dapper_header_value
api_host = os.environ.get('API_HOST', SERVICE_BRIDGE_HOST)
api_port = os.environ.get('API_PORT', API_PORT)
if ':' in api_host:
api_host = '[{}]'.format(api_host)
endpoint_url = six.moves.urllib.parse.urlunparse(
('http', '%s:%s' % (api_host, api_port), PROXY_PATH, '', '', ''))
self._state = apiproxy_rpc.RPC.RUNNING
request_kwargs = dict(
url=endpoint_url,
method='POST',
timeout=DEADLINE_DELTA_SECONDS + deadline,
headers=headers,
body=body_data)
is_lock_held = False
if six.PY2:
import imp
if imp.lock_held():
is_lock_held = True
if is_lock_held:
self.future = futures.Future()
self.future.set_result(self._SendRequestAndFinish(**request_kwargs))
else:
self.future = self.stub.thread_pool.submit(self._SendRequestAndFinish,
**request_kwargs)
def _WaitImpl(self):
assert self.future is not None
futures.wait([self.future])
return True
def _SendRequest(self, **kwargs):
try:
response = self.stub.http.request(**kwargs)
if response.status != 200:
raise apiproxy_errors.RPCFailedError(
'Proxy returned HTTP status %s %s' %
(response.status, response.reason))
except urllib3.exceptions.TimeoutError:
raise self._ErrorException(*_DEADLINE_EXCEEDED_EXCEPTION)
except (urllib3.exceptions.RequestError,
urllib3.exceptions.ConnectionError):
raise self._ErrorException(*_DEFAULT_EXCEPTION)
parsed_response = remote_api_pb2.Response.FromString(response.data)
if (parsed_response.HasField('application_error') or
parsed_response.HasField('rpc_error')):
raise self._TranslateToError(parsed_response)
self.response.ParseFromString(parsed_response.response)
def _CaptureTrace(self, f, **kwargs):
try:
f(**kwargs)
except Exception:
_, exc, tb = sys.exc_info()
self._exception = exc
self._traceback = tb
def _SendRequestAndFinish(self, **kwargs):
try:
self._CaptureTrace(self._SendRequest, **kwargs)
finally:
if self.callback:
self._CaptureTrace(self.callback)
self._state = apiproxy_rpc.RPC.FINISHING
class _UseRequestSecurityTicketLocal(threading.local):
"""Thread local holding if the default ticket should always be used."""
def __init__(self):
super(_UseRequestSecurityTicketLocal, self).__init__()
self.use_ticket_header_value = False
class DefaultApiStub(object):
"""A stub for calling services through a VM service bridge.
You can use this to stub out any service that the remote server supports.
"""
_USE_REQUEST_SECURITY_TICKET_LOCAL = _UseRequestSecurityTicketLocal()
@classmethod
def SetUseRequestSecurityTicketForThread(cls, value):
"""Sets if the in environment security ticket should be used.
Security tickets are set in the context, which gets inherited by a
child thread. Child threads should not use the security ticket of their
parent by default, because once the parent thread returns and the request
is complete, the security ticket is no longer valid.
Args:
value: Boolean value describing if we should use the security ticket.
"""
cls._USE_REQUEST_SECURITY_TICKET_LOCAL.use_ticket_header_value = value
@classmethod
def ShouldUseRequestSecurityTicketForThread(cls):
"""Gets if thie security ticket should be used for this thread."""
return cls._USE_REQUEST_SECURITY_TICKET_LOCAL.use_ticket_header_value
def __init__(self):
self.thread_pool = futures.ThreadPoolExecutor(MAX_CONCURRENT_API_CALLS)
self.http = urllib3.PoolManager(
num_pools=URLLIB3_POOL_COUNT, maxsize=URLLIB3_POOL_SIZE)
def MakeSyncCall(self, service, call, request, response):
"""Make a synchronous API call.
Args:
service: The name of the service you are trying to use.
call: The name of the method.
request: The request protocol buffer
response: The response protocol buffer to be filled.
"""
rpc = self.CreateRPC()
rpc.MakeCall(service, call, request, response)
rpc.Wait()
rpc.CheckSuccess()
def CreateRPC(self):
"""Create a new RPC object."""
return DefaultApiRPC(stub=self)
def Register(stub):
"""Insert stubs so App Engine services are accessed via the service bridge."""
apiproxy_stub_map.apiproxy.SetDefaultStub(stub)