purreal 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- purreal-0.1.0/.github/workflows/test.yml +70 -0
- purreal-0.1.0/.gitignore +175 -0
- purreal-0.1.0/KNOWN_ISSUES.md +141 -0
- purreal-0.1.0/LICENSE +674 -0
- purreal-0.1.0/PERFORMANCE_TESTING.md +250 -0
- purreal-0.1.0/PKG-INFO +483 -0
- purreal-0.1.0/PUBLISHING.md +290 -0
- purreal-0.1.0/PUBLISH_QUICK.md +75 -0
- purreal-0.1.0/README.md +443 -0
- purreal-0.1.0/STRUCTURE.md +132 -0
- purreal-0.1.0/benchmarks/README.md +50 -0
- purreal-0.1.0/benchmarks/__init__.py +1 -0
- purreal-0.1.0/benchmarks/benchmark_configs.py +318 -0
- purreal-0.1.0/benchmarks/high_throughput.py +360 -0
- purreal-0.1.0/examples/README.md +73 -0
- purreal-0.1.0/examples/__init__.py +1 -0
- purreal-0.1.0/examples/load_test.py +418 -0
- purreal-0.1.0/examples/monitor_pool.py +164 -0
- purreal-0.1.0/examples/stress_test.py +127 -0
- purreal-0.1.0/publish.bat +96 -0
- purreal-0.1.0/purreal/__init__.py +19 -0
- purreal-0.1.0/purreal/pooler.py +918 -0
- purreal-0.1.0/purreal/shutdown.py +25 -0
- purreal-0.1.0/pyproject.toml +105 -0
- purreal-0.1.0/pytest.ini +50 -0
- purreal-0.1.0/test.bat +117 -0
- purreal-0.1.0/test.sh +120 -0
- purreal-0.1.0/tests/__init__.py +0 -0
- purreal-0.1.0/tests/conftest.py +67 -0
- purreal-0.1.0/tests/test_connectivity.py +326 -0
- purreal-0.1.0/tests/test_pooler.py +609 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main, develop ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main, develop ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
16
|
+
python-version: ["3.11", "3.12"]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
cache: 'pip'
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: |
|
|
29
|
+
python -m pip install --upgrade pip
|
|
30
|
+
pip install -e ".[test]"
|
|
31
|
+
|
|
32
|
+
- name: Run tests with coverage
|
|
33
|
+
run: |
|
|
34
|
+
pytest --cov=purreal --cov-report=xml --cov-report=term-missing
|
|
35
|
+
|
|
36
|
+
- name: Upload coverage to Codecov
|
|
37
|
+
uses: codecov/codecov-action@v4
|
|
38
|
+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
|
|
39
|
+
with:
|
|
40
|
+
file: ./coverage.xml
|
|
41
|
+
flags: unittests
|
|
42
|
+
name: codecov-umbrella
|
|
43
|
+
fail_ci_if_error: false
|
|
44
|
+
|
|
45
|
+
lint:
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@v4
|
|
50
|
+
|
|
51
|
+
- name: Set up Python
|
|
52
|
+
uses: actions/setup-python@v5
|
|
53
|
+
with:
|
|
54
|
+
python-version: "3.12"
|
|
55
|
+
cache: 'pip'
|
|
56
|
+
|
|
57
|
+
- name: Install dependencies
|
|
58
|
+
run: |
|
|
59
|
+
python -m pip install --upgrade pip
|
|
60
|
+
pip install -e ".[dev]"
|
|
61
|
+
|
|
62
|
+
- name: Run black
|
|
63
|
+
run: black --check purreal tests
|
|
64
|
+
|
|
65
|
+
- name: Run ruff
|
|
66
|
+
run: ruff check purreal tests
|
|
67
|
+
|
|
68
|
+
- name: Run mypy
|
|
69
|
+
run: mypy purreal --ignore-missing-imports
|
|
70
|
+
continue-on-error: true
|
purreal-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
**/*.ipynb
|
|
6
|
+
|
|
7
|
+
# C extensions
|
|
8
|
+
*.so
|
|
9
|
+
|
|
10
|
+
# Distribution / packaging
|
|
11
|
+
.Python
|
|
12
|
+
build/
|
|
13
|
+
develop-eggs/
|
|
14
|
+
dist/
|
|
15
|
+
downloads/
|
|
16
|
+
eggs/
|
|
17
|
+
.eggs/
|
|
18
|
+
lib/
|
|
19
|
+
lib64/
|
|
20
|
+
parts/
|
|
21
|
+
sdist/
|
|
22
|
+
var/
|
|
23
|
+
wheels/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
# Usually these files are written by a python script from a template
|
|
32
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
+
*.manifest
|
|
34
|
+
*.spec
|
|
35
|
+
|
|
36
|
+
# Installer logs
|
|
37
|
+
pip-log.txt
|
|
38
|
+
pip-delete-this-directory.txt
|
|
39
|
+
|
|
40
|
+
# Unit test / coverage reports
|
|
41
|
+
htmlcov/
|
|
42
|
+
.tox/
|
|
43
|
+
.nox/
|
|
44
|
+
.coverage
|
|
45
|
+
.coverage.*
|
|
46
|
+
.cache
|
|
47
|
+
nosetests.xml
|
|
48
|
+
coverage.xml
|
|
49
|
+
*.cover
|
|
50
|
+
*.py,cover
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
cover/
|
|
54
|
+
|
|
55
|
+
# Translations
|
|
56
|
+
*.mo
|
|
57
|
+
*.pot
|
|
58
|
+
|
|
59
|
+
# Django stuff:
|
|
60
|
+
*.log
|
|
61
|
+
local_settings.py
|
|
62
|
+
db.sqlite3
|
|
63
|
+
db.sqlite3-journal
|
|
64
|
+
|
|
65
|
+
# Flask stuff:
|
|
66
|
+
instance/
|
|
67
|
+
.webassets-cache
|
|
68
|
+
|
|
69
|
+
# Scrapy stuff:
|
|
70
|
+
.scrapy
|
|
71
|
+
|
|
72
|
+
# Sphinx documentation
|
|
73
|
+
docs/_build/
|
|
74
|
+
|
|
75
|
+
# PyBuilder
|
|
76
|
+
.pybuilder/
|
|
77
|
+
target/
|
|
78
|
+
|
|
79
|
+
# Jupyter Notebook
|
|
80
|
+
.ipynb_checkpoints
|
|
81
|
+
|
|
82
|
+
# IPython
|
|
83
|
+
profile_default/
|
|
84
|
+
ipython_config.py
|
|
85
|
+
|
|
86
|
+
# pyenv
|
|
87
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
88
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
89
|
+
# .python-version
|
|
90
|
+
|
|
91
|
+
# pipenv
|
|
92
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
93
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
94
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
95
|
+
# install all needed dependencies.
|
|
96
|
+
#Pipfile.lock
|
|
97
|
+
|
|
98
|
+
# UV
|
|
99
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
100
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
101
|
+
# commonly ignored for libraries.
|
|
102
|
+
#uv.lock
|
|
103
|
+
|
|
104
|
+
# poetry
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
106
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
107
|
+
# commonly ignored for libraries.
|
|
108
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
109
|
+
#poetry.lock
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
#pdm.lock
|
|
114
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
115
|
+
# in version control.
|
|
116
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
117
|
+
.pdm.toml
|
|
118
|
+
.pdm-python
|
|
119
|
+
.pdm-build/
|
|
120
|
+
|
|
121
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
122
|
+
__pypackages__/
|
|
123
|
+
|
|
124
|
+
# Celery stuff
|
|
125
|
+
celerybeat-schedule
|
|
126
|
+
celerybeat.pid
|
|
127
|
+
|
|
128
|
+
# SageMath parsed files
|
|
129
|
+
*.sage.py
|
|
130
|
+
|
|
131
|
+
# Environments
|
|
132
|
+
.env
|
|
133
|
+
.venv
|
|
134
|
+
env/
|
|
135
|
+
venv/
|
|
136
|
+
ENV/
|
|
137
|
+
env.bak/
|
|
138
|
+
venv.bak/
|
|
139
|
+
|
|
140
|
+
# Spyder project settings
|
|
141
|
+
.spyderproject
|
|
142
|
+
.spyproject
|
|
143
|
+
|
|
144
|
+
# Rope project settings
|
|
145
|
+
.ropeproject
|
|
146
|
+
|
|
147
|
+
# mkdocs documentation
|
|
148
|
+
/site
|
|
149
|
+
|
|
150
|
+
# mypy
|
|
151
|
+
.mypy_cache/
|
|
152
|
+
.dmypy.json
|
|
153
|
+
dmypy.json
|
|
154
|
+
|
|
155
|
+
# Pyre type checker
|
|
156
|
+
.pyre/
|
|
157
|
+
|
|
158
|
+
# pytype static type analyzer
|
|
159
|
+
.pytype/
|
|
160
|
+
|
|
161
|
+
# Cython debug symbols
|
|
162
|
+
cython_debug/
|
|
163
|
+
|
|
164
|
+
# PyCharm
|
|
165
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
166
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
167
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
168
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
169
|
+
#.idea/
|
|
170
|
+
|
|
171
|
+
# Ruff stuff:
|
|
172
|
+
.ruff_cache/
|
|
173
|
+
|
|
174
|
+
# PyPI configuration file
|
|
175
|
+
.pypirc
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Known Issues
|
|
2
|
+
|
|
3
|
+
## Critical: Burst Load > max_connections Causes Timeouts
|
|
4
|
+
|
|
5
|
+
**Status:** Identified, Not Fixed
|
|
6
|
+
**Severity:** High
|
|
7
|
+
**Affects:** v0.1.0
|
|
8
|
+
|
|
9
|
+
### Description
|
|
10
|
+
|
|
11
|
+
When burst load exceeds `max_connections` (e.g., 100 concurrent requests with 50 max connections), exactly `max_connections` tasks succeed and the rest timeout after `acquisition_timeout`.
|
|
12
|
+
|
|
13
|
+
### Symptoms
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Burst: 100 concurrent queries, max_connections=50
|
|
17
|
+
Result: 50 succeed, 50 timeout after 30s
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Debug output:**
|
|
21
|
+
```
|
|
22
|
+
POOL DEBUG: Task Task-xxx TIMEOUT acquiring connection after 30.00s
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Root Cause
|
|
26
|
+
|
|
27
|
+
Waiter notification mechanism has a race condition when many tasks are waiting. Connections are released but waiting tasks are not properly notified, causing them to timeout instead of acquiring the released connections.
|
|
28
|
+
|
|
29
|
+
**Location:** `purreal/pooler.py` lines 526-531
|
|
30
|
+
```python
|
|
31
|
+
# Notify one waiter if any exist *after* pool state is updated
|
|
32
|
+
if self._connection_waiters:
|
|
33
|
+
waiter_to_notify = self._connection_waiters.popleft()
|
|
34
|
+
if not waiter_to_notify.done():
|
|
35
|
+
waiter_to_notify.set_result(None)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Workaround
|
|
39
|
+
|
|
40
|
+
**Option 1: Size pool for peak load**
|
|
41
|
+
```python
|
|
42
|
+
pool = SurrealDBConnectionPool(
|
|
43
|
+
max_connections=100, # Set to peak burst size
|
|
44
|
+
...
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Option 2: Limit concurrent requests**
|
|
49
|
+
```python
|
|
50
|
+
# Use semaphore to limit concurrency
|
|
51
|
+
semaphore = asyncio.Semaphore(50) # Match max_connections
|
|
52
|
+
|
|
53
|
+
async def rate_limited_query():
|
|
54
|
+
async with semaphore:
|
|
55
|
+
async with pool.acquire() as conn:
|
|
56
|
+
await conn.query(...)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Option 3: Batch requests**
|
|
60
|
+
```python
|
|
61
|
+
# Instead of 100 concurrent
|
|
62
|
+
for batch in chunks(requests, 50):
|
|
63
|
+
await asyncio.gather(*batch)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Impact
|
|
67
|
+
|
|
68
|
+
- ✅ **Sustained load:** Works fine (continuous workers)
|
|
69
|
+
- ✅ **Moderate bursts:** OK if burst ≤ max_connections
|
|
70
|
+
- ❌ **Large bursts:** Fails if burst > max_connections
|
|
71
|
+
- ✅ **Sequential batches:** Works fine
|
|
72
|
+
|
|
73
|
+
### Scenarios Affected
|
|
74
|
+
|
|
75
|
+
1. API endpoints receiving sudden traffic spikes > pool size
|
|
76
|
+
2. Background job processing with large queue dumps
|
|
77
|
+
3. Load testing with concurrency > max_connections
|
|
78
|
+
|
|
79
|
+
### Scenarios NOT Affected
|
|
80
|
+
|
|
81
|
+
1. Web servers with gradual ramp-up
|
|
82
|
+
2. Continuous background workers
|
|
83
|
+
3. RPC services with connection limits
|
|
84
|
+
4. Applications using semaphores for rate limiting
|
|
85
|
+
|
|
86
|
+
### Recommended Configuration
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# For production, set max_connections to expected peak + 20%
|
|
90
|
+
expected_peak = 100
|
|
91
|
+
pool = SurrealDBConnectionPool(
|
|
92
|
+
max_connections=int(expected_peak * 1.2), # 120
|
|
93
|
+
acquisition_timeout=15.0, # Reduce timeout
|
|
94
|
+
...
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Fix Plan
|
|
99
|
+
|
|
100
|
+
1. Investigate waiter notification timing
|
|
101
|
+
2. Consider notifying ALL waiters instead of one
|
|
102
|
+
3. Add tests for burst > max_connections
|
|
103
|
+
4. Validate fix with burst_size=200, max_connections=50
|
|
104
|
+
|
|
105
|
+
### Testing
|
|
106
|
+
|
|
107
|
+
To reproduce:
|
|
108
|
+
```bash
|
|
109
|
+
# This will fail
|
|
110
|
+
python -c "
|
|
111
|
+
from purreal import SurrealDBConnectionPool
|
|
112
|
+
import asyncio
|
|
113
|
+
|
|
114
|
+
async def test():
|
|
115
|
+
pool = SurrealDBConnectionPool(
|
|
116
|
+
uri='ws://localhost:8000/rpc',
|
|
117
|
+
credentials={'username': 'root', 'password': 'root'},
|
|
118
|
+
namespace='test',
|
|
119
|
+
database='test',
|
|
120
|
+
max_connections=50,
|
|
121
|
+
acquisition_timeout=30.0,
|
|
122
|
+
)
|
|
123
|
+
await pool.initialize()
|
|
124
|
+
|
|
125
|
+
async def query(i):
|
|
126
|
+
async with pool.acquire() as conn:
|
|
127
|
+
await conn.query('RETURN 1')
|
|
128
|
+
|
|
129
|
+
# 100 concurrent (2x pool size) - 50 will timeout
|
|
130
|
+
tasks = [query(i) for i in range(100)]
|
|
131
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
132
|
+
await pool.close()
|
|
133
|
+
|
|
134
|
+
asyncio.run(test())
|
|
135
|
+
"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
**Reported:** 2025-11-09
|
|
141
|
+
**Tracked:** https://github.com/dyleeeeeeee/purreal/issues/XXX
|