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

Async get_events, handle_event, handle_readables, handle_writables #769

Merged
merged 61 commits into from
Nov 23, 2021
Merged
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
8d87f7e
Asynchronous `handle_event` and `LocalExecutor` thread
abhinavsingh Nov 20, 2021
dbaf18a
Bail out on first task completion
abhinavsingh Nov 20, 2021
40e38cb
mypy
abhinavsingh Nov 20, 2021
e0fd4de
Add `helper/benchmark.sh` and fix threaded which must now use asyncio…
abhinavsingh Nov 20, 2021
f5998dd
Print open file diff from `benchmark.sh`
abhinavsingh Nov 21, 2021
d5ef5fd
Add `--local-executor` flag, disabled by default for now until tests …
abhinavsingh Nov 21, 2021
aad066b
Async `handle_readables` and `handle_writables` for `HttpProtocolHand…
abhinavsingh Nov 21, 2021
b25b041
Async `get_events`
abhinavsingh Nov 21, 2021
f0042de
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 21, 2021
2d311a6
Address tests after async changes
abhinavsingh Nov 21, 2021
416d852
mypy and flake8
abhinavsingh Nov 21, 2021
c71f91f
spelldoc
abhinavsingh Nov 21, 2021
94f9ef8
`check.py` and trailing comma
abhinavsingh Nov 21, 2021
ae089f4
Rename to `_assertions.py`
abhinavsingh Nov 21, 2021
b3fb2bd
Merge branch 'async-handle-events' of github.com:abhinavsingh/proxy.p…
abhinavsingh Nov 21, 2021
2a727c8
Add missing `pytest-mock` and `pytest-asyncio` deps
abhinavsingh Nov 21, 2021
c03c817
Add `pytest-mock` to `pylint` deps
abhinavsingh Nov 21, 2021
78b59aa
Correct use of `parameterize` and add `PT007` to flake8 ignores
abhinavsingh Nov 21, 2021
55d3a29
Fix mypy hints broken for `< Python3.9`
abhinavsingh Nov 21, 2021
87ff921
Remove usage of `asynccontextmanager` which is not available for all …
abhinavsingh Nov 21, 2021
9436e99
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 21, 2021
39eebc4
Fix for pre-python-3.9 versions
abhinavsingh Nov 21, 2021
f0d4357
Merge branch 'async-handle-events' of github.com:abhinavsingh/proxy.p…
abhinavsingh Nov 21, 2021
d89160c
`AsyncTask` apis `set_name` and `get_name` are not available on all s…
abhinavsingh Nov 21, 2021
0543b54
Install setuptools via `lib-dep` until we recommend editable install
abhinavsingh Nov 21, 2021
cbd2be2
Deprecate support for `Python 3.6`
abhinavsingh Nov 21, 2021
0c30899
Use recommendation suggested here https://github.com/abhinavsingh/pro…
abhinavsingh Nov 21, 2021
5348b0a
Address recommendation here https://github.com/abhinavsingh/proxy.py/…
abhinavsingh Nov 21, 2021
5dd0926
Make `Threadless` agnostic of `multiprocessing.Process`
abhinavsingh Nov 21, 2021
3b8019d
Acceptors must dispatch to local executor in non-blocking fashion
abhinavsingh Nov 21, 2021
3e7ddec
No daemon for executor processes and fix shutdown logic
abhinavsingh Nov 21, 2021
1c746e6
Only return fds from `_selected_events` not all events data
abhinavsingh Nov 21, 2021
1f4c176
Refactor logic
abhinavsingh Nov 21, 2021
91741a7
Prefix private methods with `_`
abhinavsingh Nov 21, 2021
3044758
`work_queue` and not `client_queue`
abhinavsingh Nov 21, 2021
5b04f42
Turn `Threadless` into an abstract executor. Introduce `RemoteExecutor`
abhinavsingh Nov 22, 2021
36dcbc1
Make `LocalExecutor` agnostic of `threading.Thread`
abhinavsingh Nov 22, 2021
271652c
`LocalExecutor` now implements `Threadless`
abhinavsingh Nov 22, 2021
194ce58
`get_events` and `get_descriptors` now must return int and not sock. …
abhinavsingh Nov 22, 2021
01d5121
Fix `main` tests
abhinavsingh Nov 22, 2021
473fe8c
Apply suggestions from code review
abhinavsingh Nov 22, 2021
466530c
Merge branch 'async-handle-events' of github.com:abhinavsingh/proxy.p…
abhinavsingh Nov 22, 2021
baff0b9
Apply code review recommendations manually
abhinavsingh Nov 22, 2021
c5ed3e3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 22, 2021
15966ac
Revert back `Any` and use `addr or None`
abhinavsingh Nov 22, 2021
b870038
Merge branch 'async-handle-events' of github.com:abhinavsingh/proxy.p…
abhinavsingh Nov 22, 2021
292ea7a
Merge branch 'develop' into async-handle-events
abhinavsingh Nov 22, 2021
cedf02d
Address `flake8`
abhinavsingh Nov 22, 2021
d96d5c2
Update tests to use `fileno`
abhinavsingh Nov 22, 2021
2805841
Fix doc build
abhinavsingh Nov 22, 2021
7d5e955
Fix doc spell, use tear down and not teardown
abhinavsingh Nov 22, 2021
71a595e
Merge branch 'develop' into async-handle-events
abhinavsingh Nov 22, 2021
f36ab2d
Doc updates
abhinavsingh Nov 22, 2021
f78fe5e
Add back support for `Python 3.6`
abhinavsingh Nov 22, 2021
d158d67
Merge branch 'async-handle-events' of github.com:abhinavsingh/proxy.p…
abhinavsingh Nov 22, 2021
265380d
Acceptors dont need loop initialization
abhinavsingh Nov 22, 2021
5684b8c
On Python 3.6 `asyncio.new_event_loop()` is necessary
abhinavsingh Nov 22, 2021
8686093
Make doc happy
abhinavsingh Nov 22, 2021
d84d3b4
`--threaded` needs a new event loop for 3.7 too
abhinavsingh Nov 22, 2021
7090764
Always use `asyncio.new_event_loop()` for threaded mode
abhinavsingh Nov 22, 2021
bb0ae7c
Lint fixes
abhinavsingh Nov 23, 2021
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
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"python.linting.pylintEnabled": true,
"python.linting.pylintArgs": ["--generate-members"],
"python.linting.flake8Enabled": true,
"python.linting.flake8Args": ["--config", ".flake8"],
"python.linting.mypyEnabled": true,
"python.formatting.provider": "autopep8",
"autoDocstring.docstringFormat": "sphinx"
Expand Down
11 changes: 7 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ endif
.PHONY: container container-run container-release
.PHONY: devtools dashboard dashboard-clean

all:
echo $(IMAGE_TAG)
# lib-test
all: lib-test

https-certificates:
# Generate server key
Expand Down Expand Up @@ -94,7 +92,8 @@ lib-dep:
-r requirements.txt \
-r requirements-testing.txt \
-r requirements-release.txt \
-r requirements-tunnel.txt \
-r requirements-tunnel.txt && \
pip install "setuptools>=42"

lib-lint:
python -m tox -e lint
Expand Down Expand Up @@ -128,6 +127,7 @@ lib-coverage:
$(OPEN) htmlcov/index.html

lib-profile:
ulimit -n 65536 && \
sudo py-spy record \
-o profile.svg \
-t -F -s -- \
Expand All @@ -137,6 +137,9 @@ lib-profile:
--disable-http-proxy \
--enable-web-server \
--plugin proxy.plugin.WebServerPlugin \
--local-executor \
--backlog 65536 \
--open-file-limit 65536
--log-file /dev/null

devtools:
Expand Down
102 changes: 71 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
- [Setup Local Environment](#setup-local-environment)
- [Setup Git Hooks](#setup-git-hooks)
- [Sending a Pull Request](#sending-a-pull-request)
- [Benchmarks](#benchmarks)
- [Flags](#flags)
- [Changelog](#changelog)
- [v2.x](#v2x)
Expand All @@ -126,36 +127,56 @@

```console
# On Macbook Pro 2019 / 2.4 GHz 8-Core Intel Core i9 / 32 GB RAM
❯ hey -n 10000 -c 100 http://localhost:8899/http-route-example

Summary:
Total: 0.3248 secs
Slowest: 0.1007 secs
Fastest: 0.0002 secs
Average: 0.0028 secs
Requests/sec: 30784.7958

Total data: 190000 bytes
Size/request: 19 bytes

Response time histogram:
0.000 [1] |
0.010 [9533] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.020 [384] |■■

Latency distribution:
10% in 0.0004 secs
25% in 0.0007 secs
50% in 0.0013 secs
75% in 0.0029 secs
90% in 0.0057 secs
95% in 0.0097 secs
99% in 0.0185 secs

Status code distribution:
[200] 10000 responses
❯ ./helper/benchmark.sh
CONCURRENCY: 100 workers, TOTAL REQUESTS: 100000 req, QPS: 5000 req/sec, TIMEOUT: 1 sec

Summary:
Total: 3.1560 secs
Slowest: 0.0375 secs
Fastest: 0.0006 secs
Average: 0.0031 secs
Requests/sec: 31685.9140

Total data: 1900000 bytes
Size/request: 19 bytes

Response time histogram:
0.001 [1] |
0.004 [91680] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.008 [7929] |■■■
0.012 [263] |
0.015 [29] |
0.019 [8] |
0.023 [23] |
0.026 [15] |
0.030 [27] |
0.034 [16] |
0.037 [9] |


Latency distribution:
10% in 0.0022 secs
25% in 0.0025 secs
50% in 0.0029 secs
75% in 0.0034 secs
90% in 0.0041 secs
95% in 0.0048 secs
99% in 0.0066 secs

Details (average, fastest, slowest):
DNS+dialup: 0.0000 secs, 0.0006 secs, 0.0375 secs
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
req write: 0.0000 secs, 0.0000 secs, 0.0046 secs
resp wait: 0.0030 secs, 0.0006 secs, 0.0320 secs
resp read: 0.0000 secs, 0.0000 secs, 0.0029 secs

Status code distribution:
[200] 100000 responses
```

PS: `proxy.py` and benchmark tools are running on the same machine during the above load test.
Checkout the repo and try it for yourself. See [Benchmarks](#benchmarks) for more details.

- Lightweight
- Uses only `~5-20MB` RAM
- No external dependency other than standard Python library
Expand Down Expand Up @@ -1977,13 +1998,21 @@ Every pull request is tested using GitHub actions.
See [GitHub workflow](https://github.com/abhinavsingh/proxy.py/tree/develop/.github/workflows)
for list of tests.

# Benchmarks

Simply run the following command from repo root to start benchmark

```console
❯ ./helper/benchmark.sh
```

# Flags

```console
❯ proxy -h
usage: -m [-h] [--enable-events] [--enable-conn-pool] [--threadless]
[--threaded] [--num-workers NUM_WORKERS] [--backlog BACKLOG]
[--hostname HOSTNAME] [--port PORT]
[--threaded] [--num-workers NUM_WORKERS] [--local-executor]
[--backlog BACKLOG] [--hostname HOSTNAME] [--port PORT]
[--unix-socket-path UNIX_SOCKET_PATH]
[--num-acceptors NUM_ACCEPTORS] [--version] [--log-level LOG_LEVEL]
[--log-file LOG_FILE] [--log-format LOG_FORMAT]
Expand All @@ -2009,7 +2038,7 @@ usage: -m [-h] [--enable-events] [--enable-conn-pool] [--threadless]
[--filtered-url-regex-config FILTERED_URL_REGEX_CONFIG]
[--cloudflare-dns-mode CLOUDFLARE_DNS_MODE]

proxy.py v2.3.2
proxy.py v2.3.2.dev193+g87ff921.d20211121

options:
-h, --help show this help message and exit
Expand All @@ -2026,6 +2055,13 @@ options:
handle each client connection.
--num-workers NUM_WORKERS
Defaults to number of CPU cores.
--local-executor Default: False. Disabled by default. When enabled
acceptors will make use of local (same process)
executor instead of distributing load across remote
(other process) executors. Enable this option to
achieve CPU affinity between acceptors and executors,
instead of using underlying OS kernel scheduling
algorithm.
--backlog BACKLOG Default: 100. Maximum number of pending connections to
proxy server
--hostname HOSTNAME Default: ::1. Server IP address.
Expand Down Expand Up @@ -2155,6 +2191,10 @@ https://github.com/abhinavsingh/proxy.py/issues/new

# Changelog

## v2.4.0

- No longer support `Python 3.6` due to `asyncio.run` usage in the core.

## v2.x

- No longer ~~a single file module~~.
Expand Down
24 changes: 13 additions & 11 deletions check.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,18 @@
sys.exit(1)

# Update README.md flags section to match current library --help output
abhinavsingh marked this conversation as resolved.
Show resolved Hide resolved
# lib_help = subprocess.check_output(
# ['python', '-m', 'proxy', '-h']
# )
# with open('README.md', 'rb+') as f:
# c = f.read()
# pre_flags, post_flags = c.split(b'# Flags')
# help_text, post_changelog = post_flags.split(b'# Changelog')
# f.seek(0)
# f.write(pre_flags + b'# Flags\n\n```console\n\xe2\x9d\xaf proxy -h\n' + lib_help + b'```' +
# b'\n# Changelog' + post_changelog)
lib_help = subprocess.check_output(
['python', '-m', 'proxy', '-h'],
)
with open('README.md', 'rb+') as f:
c = f.read()
pre_flags, post_flags = c.split(b'# Flags')
help_text, post_changelog = post_flags.split(b'# Changelog')
f.seek(0)
f.write(
pre_flags + b'# Flags\n\n```console\n\xe2\x9d\xaf proxy -h\n' + lib_help + b'```' +
b'\n\n# Changelog' + post_changelog,
)

# Version is also hardcoded in README.md flags section
readme_version_cmd = 'cat README.md | grep "proxy.py v" | tail -2 | head -1 | cut -d " " -f 2 | cut -c2-'
Expand All @@ -72,7 +74,7 @@
# Doesn't contain "v" prefix
readme_version = readme_version_output.decode().strip()

if readme_version != lib_version[1:].split('-')[0]:
if readme_version != lib_version:
print(
'Version mismatch found. {0} (readme) vs {1} (lib).'.format(
readme_version, lib_version,
Expand Down
14 changes: 11 additions & 3 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,11 @@

nitpicky = True
_any_role = 'any'
_py_obj_role = 'py:obj'
_py_class_role = 'py:class'
nitpick_ignore = [
(_any_role, '<proxy.HttpProxyBasePlugin>'),
(_any_role, '__init__'),
(_any_role, '--threadless'),
(_any_role, 'Client'),
(_any_role, 'event_queue'),
(_any_role, 'fd_queue'),
Expand All @@ -256,8 +256,10 @@
(_any_role, 'HttpParser.state'),
(_any_role, 'HttpProtocolHandler'),
(_any_role, 'multiprocessing.Manager'),
(_any_role, 'work_klass'),
(_any_role, 'proxy.core.base.tcp_upstream.TcpUpstreamConnectionHandler'),
(_any_role, 'work_klass'),
(_py_class_role, '_asyncio.Task'),
(_py_class_role, 'asyncio.events.AbstractEventLoop'),
(_py_class_role, 'CacheStore'),
(_py_class_role, 'HttpParser'),
(_py_class_role, 'HttpProtocolHandlerPlugin'),
Expand All @@ -268,11 +270,17 @@
(_py_class_role, 'paramiko.channel.Channel'),
(_py_class_role, 'proxy.http.parser.parser.T'),
(_py_class_role, 'proxy.plugin.cache.store.base.CacheStore'),
(_py_class_role, 'proxy.core.pool.AcceptorPool'),
(_py_class_role, 'proxy.core.executors.ThreadlessPool'),
(_py_class_role, 'proxy.core.acceptor.threadless.T'),
(_py_class_role, 'queue.Queue[Any]'),
(_py_class_role, 'TcpClientConnection'),
(_py_class_role, 'TcpServerConnection'),
(_py_class_role, 'unittest.case.TestCase'),
(_py_class_role, 'unittest.result.TestResult'),
(_py_class_role, 'UUID'),
(_py_class_role, 'WebsocketFrame'),
(_py_class_role, 'Url'),
(_py_class_role, 'WebsocketFrame'),
(_py_class_role, 'Work'),
(_py_obj_role, 'proxy.core.acceptor.threadless.T'),
]
5 changes: 2 additions & 3 deletions examples/web_scraper.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
:license: BSD, see LICENSE for more details.
"""
import time
import socket

from typing import Dict

Expand Down Expand Up @@ -40,11 +39,11 @@ class WebScraper(Work):
only PUBSUB protocol.
"""

def get_events(self) -> Dict[socket.socket, int]:
async def get_events(self) -> Dict[int, int]:
"""Return sockets and events (read or write) that we are interested in."""
return {}

def handle_events(
async def handle_events(
self,
readables: Readables,
writables: Writables,
Expand Down
77 changes: 77 additions & 0 deletions helper/benchmark.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/bin/bash
#
# proxy.py
# ~~~~~~~~
# ⚡⚡⚡ Fast, Lightweight, Programmable, TLS interception capable
# proxy server for Application debugging, testing and development.
#
# :copyright: (c) 2013-present by Abhinav Singh and contributors.
# :license: BSD, see LICENSE for more details.
#
usage() {
echo "Usage: ./helper/benchmark.sh"
echo "You must run this script from proxy.py repo root."
}

DIRNAME=$(dirname "$0")
if [ "$DIRNAME" != "./helper" ]; then
usage
exit 1
fi

BASENAME=$(basename "$0")
if [ "$BASENAME" != "benchmark.sh" ]; then
usage
exit 1
fi

PWD=$(pwd)
if [ $(basename $PWD) != "proxy.py" ]; then
usage
exit 1
fi

TIMEOUT=1
QPS=20000
CONCURRENCY=100
TOTAL_REQUESTS=100000
OPEN_FILE_LIMIT=65536
BACKLOG=OPEN_FILE_LIMIT
PID_FILE=/tmp/proxy.pid

ulimit -n $OPEN_FILE_LIMIT

# time python -m \
# proxy \
# --enable-web-server \
# --plugin proxy.plugin.WebServerPlugin \
# --backlog $BACKLOG \
# --open-file-limit $OPEN_FILE_LIMIT \
# --pid-file $PID_FILE \
# --log-file /dev/null

PID=$(cat $PID_FILE)
if [[ -z "$PID" ]]; then
echo "Either pid file doesn't exist or no pid found in the pid file"
exit 1
fi
ADDR=$(lsof -Pan -p $PID -i | grep -v COMMAND | awk '{ print $9 }')

PRE_RUN_OPEN_FILES=$(./helper/monitor_open_files.sh)

echo "CONCURRENCY: $CONCURRENCY workers, TOTAL REQUESTS: $TOTAL_REQUESTS req, QPS: $QPS req/sec, TIMEOUT: $TIMEOUT sec"
hey \
-n $TOTAL_REQUESTS \
-c $CONCURRENCY \
-q $QPS \
-t $TIMEOUT \
http://$ADDR/http-route-example

POST_RUN_OPEN_FILES=$(./helper/monitor_open_files.sh)

echo $output

echo "Open files diff:"
diff <( echo "$PRE_RUN_OPEN_FILES" ) <( echo "$POST_RUN_OPEN_FILES" )

# while true; do netstat -ant | grep .8899 | awk '{print $6}' | sort | uniq -c | sort -n; sleep 1; done
2 changes: 1 addition & 1 deletion helper/monitor_open_files.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/bin/bash

#
# proxy.py
# ~~~~~~~~
# ⚡⚡⚡ Fast, Lightweight, Programmable, TLS interception capable
Expand Down
Loading