Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support XML-RPC multicall #3778

Merged
merged 6 commits into from
Apr 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions tests/unit/legacy/api/xmlrpc/test_xmlrpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -792,3 +792,112 @@ def test_browse(db_request):
"Programming Language :: Python",
],
)) == {(expected_release.name, expected_release.version)}


class TestMulticall:

def test_multicall(self, monkeypatch):
dumped = pretend.stub(encode=lambda: None)
dumps = pretend.call_recorder(lambda *a, **kw: dumped)
monkeypatch.setattr(xmlrpc.xmlrpc.client, 'dumps', dumps)

loaded = pretend.stub()
loads = pretend.call_recorder(lambda *a, **kw: loaded)
monkeypatch.setattr(xmlrpc.xmlrpc.client, 'loads', loads)

subreq = pretend.stub()
blank = pretend.call_recorder(lambda *a, **kw: subreq)
monkeypatch.setattr(xmlrpc.Request, 'blank', blank)

body = pretend.stub()
response = pretend.stub(body=body)

request = pretend.stub(
invoke_subrequest=pretend.call_recorder(lambda *a, **kw: response),
add_response_callback=pretend.call_recorder(
lambda *a, **kw: response),
)

callback = pretend.stub()
monkeypatch.setattr(
xmlrpc,
'measure_response_content_length',
pretend.call_recorder(lambda metric_name: callback)
)

args = [
{'methodName': 'search', 'params': [{'name': 'foo'}]},
{'methodName': 'browse', 'params': [{'classifiers': ['bar']}]},
]

responses = xmlrpc.multicall(request, args)

assert responses == [loaded, loaded]
assert blank.calls == [
pretend.call('/RPC2', headers={'Content-Type': 'text/xml'}),
pretend.call('/RPC2', headers={'Content-Type': 'text/xml'}),
]
assert request.invoke_subrequest.calls == [
pretend.call(subreq, use_tweens=True),
pretend.call(subreq, use_tweens=True),
]
assert request.add_response_callback.calls == [
pretend.call(callback),
]
assert dumps.calls == [
pretend.call(({'name': 'foo'},), methodname='search'),
pretend.call(({'classifiers': ['bar']},), methodname='browse'),
]
assert loads.calls == [pretend.call(body), pretend.call(body)]

def test_recursive_multicall(self):
request = pretend.stub()
args = [
{'methodName': 'system.multicall', 'params': []},
]
with pytest.raises(xmlrpc.XMLRPCWrappedError) as exc:
xmlrpc.multicall(request, args)

assert exc.value.faultString == (
'ValueError: Cannot use system.multicall inside a multicall'
)

def test_missing_multicall_method(self):
request = pretend.stub()
args = [{}]
with pytest.raises(xmlrpc.XMLRPCWrappedError) as exc:
xmlrpc.multicall(request, args)

assert exc.value.faultString == (
'ValueError: Method name not provided'
)

def test_too_many_multicalls_method(self):
request = pretend.stub()
args = [{'methodName': 'nah'}] * 21

with pytest.raises(xmlrpc.XMLRPCWrappedError) as exc:
xmlrpc.multicall(request, args)

assert exc.value.faultString == (
'ValueError: Multicall limit is 20 calls'
)

def test_measure_response_content_length(self):
metric_name = 'some_metric_name'
callback = xmlrpc.measure_response_content_length(metric_name)

request = pretend.stub(
registry=pretend.stub(
datadog=pretend.stub(
histogram=pretend.call_recorder(lambda *a: None)
)
)
)
response = pretend.stub(content_length=pretend.stub())

callback(request, response)

assert request.registry.datadog.histogram.calls == [
pretend.call(metric_name, response.content_length),
]
51 changes: 51 additions & 0 deletions warehouse/legacy/api/xmlrpc/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
import collections.abc
import datetime
import functools
import xmlrpc.client
import xmlrpc.server

from elasticsearch_dsl import Q
from packaging.utils import canonicalize_name
from pyramid.request import Request
from pyramid.view import view_config
from pyramid_rpc.xmlrpc import (
exception_view as _exception_view, xmlrpc_method as _xmlrpc_method
Expand All @@ -31,6 +33,9 @@
)


_MAX_MULTICALLS = 20


def xmlrpc_method(**kwargs):
"""
Support multiple endpoints serving the same views by chaining calls to
Expand Down Expand Up @@ -441,3 +446,49 @@ def browse(request, classifiers):
)

return [(r.name, r.version) for r in releases]


def measure_response_content_length(metric_name):

def _callback(request, response):
request.registry.datadog.histogram(
metric_name, response.content_length
)

return _callback


@xmlrpc_method(method='system.multicall')
def multicall(request, args):
if any(arg.get('methodName') == 'system.multicall' for arg in args):
raise XMLRPCWrappedError(
ValueError('Cannot use system.multicall inside a multicall')
)

if not all(arg.get('methodName') for arg in args):
raise XMLRPCWrappedError(ValueError('Method name not provided'))

if len(args) > _MAX_MULTICALLS:
raise XMLRPCWrappedError(
ValueError(f'Multicall limit is {_MAX_MULTICALLS} calls')
)

responses = []
for arg in args:
name = arg.get('methodName')
subreq = Request.blank('/RPC2', headers={'Content-Type': 'text/xml'})
subreq.method = 'POST'
subreq.body = xmlrpc.client.dumps(
tuple(arg.get('params')),
methodname=name,
).encode()
response = request.invoke_subrequest(subreq, use_tweens=True)
responses.append(xmlrpc.client.loads(response.body))

request.add_response_callback(
measure_response_content_length(
'warehouse.xmlrpc.system.multicall.content_length'
)
)

return responses