picopyn 2.0.0.dev1__tar.gz → 2.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.
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/AUTHORS.md +2 -1
- {picopyn-2.0.0.dev1/picopyn.egg-info → picopyn-2.1.0}/PKG-INFO +13 -12
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/README.md +12 -11
- picopyn-2.1.0/docs/css/deprecated.css +21 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/diagrams/_pool_init_discovery_disabled.md +1 -1
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/diagrams/_pool_init_discovery_enabled.md +1 -1
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/diagrams/_pool_lifecycle.md +4 -4
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/diagrams/_query_metadata_pool_init.md +2 -2
- picopyn-2.1.0/docs/diagrams/_rebalance_pool_after_reconcile.md +50 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/diagrams/_reconcile_pool_with_topology.md +12 -4
- picopyn-2.1.0/docs/diagrams/_reconcile_rebalance.md +45 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/diagrams/_topology_tracking.md +1 -1
- picopyn-2.1.0/docs/explain.md +21 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/index.md +13 -10
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/installation.md +1 -1
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/sync.md +4 -4
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/asynchronous/client.py +9 -1
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/asynchronous/connection.py +45 -9
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/asynchronous/pool.py +518 -84
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/asynchronous/topology_tracker.py +7 -7
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/bucket.py +67 -4
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/explain.py +160 -1
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/synchronous/__init__.py +8 -1
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/synchronous/client.py +8 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/synchronous/connection.py +14 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/synchronous/pool.py +35 -35
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/topology.py +13 -8
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/utils/__init__.py +8 -3
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/utils/discovery.py +16 -3
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/utils/dsn.py +28 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/utils/queries.py +1 -8
- {picopyn-2.0.0.dev1 → picopyn-2.1.0/picopyn.egg-info}/PKG-INFO +13 -12
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn.egg-info/SOURCES.txt +3 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/pyproject.toml +1 -1
- picopyn-2.0.0.dev1/docs/explain.md +0 -11
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/LICENSE +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/MANIFEST.in +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/benchmark/README.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/benchmark/bench.py +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/benchmark/examples/10k_rows_1_thread_1_batch/overall_speed_time.png +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/benchmark/examples/10k_rows_1_thread_1_batch/threads_speed_time.png +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/benchmark/examples/1m_rows_10_threads_100_batch/errors_cumulative.png +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/benchmark/examples/1m_rows_10_threads_100_batch/overall_speed_time.png +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/benchmark/examples/1m_rows_10_threads_100_batch/threads_speed_time.png +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/benchmark/generate_data_to_csv.py +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/async.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/async_internal.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/bucket.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/changelog.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/contributing.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/diagrams/_pool_init.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/diagrams/_query_metadata_usage_lookup.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/diagrams/_reconcile_node_outside_pool_died.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/diagrams/_reconcile_pooled_node_died_size_eq.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/diagrams/_reconcile_pooled_node_died_size_gt.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/diagrams/_reconcile_pooled_node_died_size_lt.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/diagrams/_request_if_missing.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/diagrams/_topology_apply_bucket_event.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/diagrams/_topology_apply_bucket_replicaset_event.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/diagrams/_topology_components.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/diagrams/_topology_refresh.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/query_metadata.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/docs/topology.md +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/__init__.py +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/asynchronous/__init__.py +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/asynchronous/metadata_service.py +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/query_metadata.py +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/ssl.py +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/synchronous/cursor.py +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/synchronous/errors.py +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn/utils/topology.py +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn.egg-info/dependency_links.txt +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn.egg-info/requires.txt +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/picopyn.egg-info/top_level.txt +0 -0
- {picopyn-2.0.0.dev1 → picopyn-2.1.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: picopyn
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Python connector for working with the distributed Picodata database.
|
|
5
5
|
Author-email: Asya Lomakina <a.lomakina@picodata.io>, Igor Kuznetsov <i.kuznetsov@picodata.io>, Dmitriy Koltsov <dkoltsov@picodata.io>, Daniil Gorlyakov <d.gorlyakov@picodata.io>
|
|
6
6
|
License-Expression: BSD-2-Clause
|
|
@@ -55,7 +55,7 @@ Dynamic: license-file
|
|
|
55
55
|
| >=25.2.1, <25.4.4 | 0.1.1 |
|
|
56
56
|
| >=25.4.4, <25.5.1 | 0.2.0 |
|
|
57
57
|
| >=25.5.1, <26.1.x | 1.0.0 |
|
|
58
|
-
| >=26.1.1, <26.2.x | 2.0.0
|
|
58
|
+
| >=26.1.1, <26.2.x | 2.0.0, 2.1.0 |
|
|
59
59
|
| 26.2.x | ⚠️ Unreleased yet |
|
|
60
60
|
|
|
61
61
|
## Features
|
|
@@ -64,8 +64,9 @@ Dynamic: license-file
|
|
|
64
64
|
* Pluggable load-balancing strategies
|
|
65
65
|
* Asynchronous API based on [asyncpg](https://github.com/MagicStack/asyncpg)
|
|
66
66
|
* (async only) Shard-aware routing
|
|
67
|
-
* (async only) Automatic pool membership reconciliation
|
|
68
|
-
|
|
67
|
+
* (async only) Automatic pool membership reconciliation, including rebalancing a
|
|
68
|
+
full pool across the nodes
|
|
69
|
+
* Synchronous [DB-API 2.0-compatible](https://peps.python.org/pep-0249/) API based on [psycopg](https://www.psycopg.org/)
|
|
69
70
|
|
|
70
71
|
## Navigation
|
|
71
72
|
* [Installation](#installation)
|
|
@@ -97,24 +98,24 @@ Async:
|
|
|
97
98
|
|
|
98
99
|
```python
|
|
99
100
|
import asyncio
|
|
100
|
-
from picopyn.asynchronous import
|
|
101
|
+
from picopyn.asynchronous import Pool
|
|
101
102
|
|
|
102
103
|
async def main():
|
|
103
|
-
# create and
|
|
104
|
-
|
|
105
|
-
await
|
|
104
|
+
# create and open pool to the picodata cluster
|
|
105
|
+
pool = Pool(dsn="postgresql://admin:pass@localhost:5432", enable_discovery=True)
|
|
106
|
+
await pool.open()
|
|
106
107
|
|
|
107
108
|
# execute DDL operations
|
|
108
|
-
await
|
|
109
|
+
await pool.execute('''
|
|
109
110
|
CREATE TABLE "warehouse" (id INTEGER NOT NULL, item TEXT NOT NULL, PRIMARY KEY (id)) USING memtx DISTRIBUTED BY (id) OPTION (TIMEOUT = 3.0);
|
|
110
111
|
''')
|
|
111
112
|
|
|
112
113
|
# execute DML/DQL operations
|
|
113
|
-
await
|
|
114
|
-
rows = await
|
|
114
|
+
await pool.execute('INSERT INTO "warehouse" VALUES ($1::int, $2::varchar)', 1, "test")
|
|
115
|
+
rows = await pool.fetch('SELECT * FROM "warehouse"')
|
|
115
116
|
print(rows)
|
|
116
117
|
|
|
117
|
-
await
|
|
118
|
+
await pool.close()
|
|
118
119
|
|
|
119
120
|
asyncio.run(main())
|
|
120
121
|
```
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
| >=25.2.1, <25.4.4 | 0.1.1 |
|
|
13
13
|
| >=25.4.4, <25.5.1 | 0.2.0 |
|
|
14
14
|
| >=25.5.1, <26.1.x | 1.0.0 |
|
|
15
|
-
| >=26.1.1, <26.2.x | 2.0.0
|
|
15
|
+
| >=26.1.1, <26.2.x | 2.0.0, 2.1.0 |
|
|
16
16
|
| 26.2.x | ⚠️ Unreleased yet |
|
|
17
17
|
|
|
18
18
|
## Features
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
* Pluggable load-balancing strategies
|
|
22
22
|
* Asynchronous API based on [asyncpg](https://github.com/MagicStack/asyncpg)
|
|
23
23
|
* (async only) Shard-aware routing
|
|
24
|
-
* (async only) Automatic pool membership reconciliation
|
|
25
|
-
|
|
24
|
+
* (async only) Automatic pool membership reconciliation, including rebalancing a
|
|
25
|
+
full pool across the nodes
|
|
26
|
+
* Synchronous [DB-API 2.0-compatible](https://peps.python.org/pep-0249/) API based on [psycopg](https://www.psycopg.org/)
|
|
26
27
|
|
|
27
28
|
## Navigation
|
|
28
29
|
* [Installation](#installation)
|
|
@@ -54,24 +55,24 @@ Async:
|
|
|
54
55
|
|
|
55
56
|
```python
|
|
56
57
|
import asyncio
|
|
57
|
-
from picopyn.asynchronous import
|
|
58
|
+
from picopyn.asynchronous import Pool
|
|
58
59
|
|
|
59
60
|
async def main():
|
|
60
|
-
# create and
|
|
61
|
-
|
|
62
|
-
await
|
|
61
|
+
# create and open pool to the picodata cluster
|
|
62
|
+
pool = Pool(dsn="postgresql://admin:pass@localhost:5432", enable_discovery=True)
|
|
63
|
+
await pool.open()
|
|
63
64
|
|
|
64
65
|
# execute DDL operations
|
|
65
|
-
await
|
|
66
|
+
await pool.execute('''
|
|
66
67
|
CREATE TABLE "warehouse" (id INTEGER NOT NULL, item TEXT NOT NULL, PRIMARY KEY (id)) USING memtx DISTRIBUTED BY (id) OPTION (TIMEOUT = 3.0);
|
|
67
68
|
''')
|
|
68
69
|
|
|
69
70
|
# execute DML/DQL operations
|
|
70
|
-
await
|
|
71
|
-
rows = await
|
|
71
|
+
await pool.execute('INSERT INTO "warehouse" VALUES ($1::int, $2::varchar)', 1, "test")
|
|
72
|
+
rows = await pool.fetch('SELECT * FROM "warehouse"')
|
|
72
73
|
print(rows)
|
|
73
74
|
|
|
74
|
-
await
|
|
75
|
+
await pool.close()
|
|
75
76
|
|
|
76
77
|
asyncio.run(main())
|
|
77
78
|
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/* mkdocstrings renders a docstring "Deprecated:" section as <details class="deprecated">.
|
|
2
|
+
That class is not part of Material's admonition set, so it falls back to the neutral
|
|
3
|
+
blue "note" look and deprecations read like ordinary remarks. Paint it as a warning. */
|
|
4
|
+
|
|
5
|
+
.md-typeset details.deprecated {
|
|
6
|
+
border-color: #ff9100;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.md-typeset details.deprecated:focus-within {
|
|
10
|
+
box-shadow: 0 0 0 0.2rem #ff91001a;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.md-typeset details.deprecated > summary {
|
|
14
|
+
background-color: #ff91001a;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.md-typeset details.deprecated > summary::before {
|
|
18
|
+
background-color: #ff9100;
|
|
19
|
+
-webkit-mask-image: var(--md-admonition-icon--warning);
|
|
20
|
+
mask-image: var(--md-admonition-icon--warning);
|
|
21
|
+
}
|
|
@@ -17,7 +17,7 @@ ERROR([Initialization failed]):::error
|
|
|
17
17
|
CFG[/"Pool settings<br>· DSN string<br>· pool size<br>· forbidden tiers<br>· cluster discovery disabled"/]
|
|
18
18
|
|
|
19
19
|
GENERAL_DETAILS_NOTE>"Return to general algorithm"]:::note
|
|
20
|
-
click GENERAL_DETAILS_NOTE "#picopyn.asynchronous.Pool.
|
|
20
|
+
click GENERAL_DETAILS_NOTE "#picopyn.asynchronous.Pool.open" "Return to general algorithm"
|
|
21
21
|
|
|
22
22
|
%% === Data objects ===
|
|
23
23
|
B_DSN[[DSN node list]]
|
|
@@ -18,7 +18,7 @@ ERROR([Initialization failed]):::error
|
|
|
18
18
|
CFG[/"Pool settings<br>· DSN string<br>· pool_size<br>· forbidden tiers<br>· cluster discovery enabled"/]
|
|
19
19
|
|
|
20
20
|
GENERAL_DETAILS_NOTE>"Return to general algorithm"]:::note
|
|
21
|
-
click GENERAL_DETAILS_NOTE "#picopyn.asynchronous.Pool.
|
|
21
|
+
click GENERAL_DETAILS_NOTE "#picopyn.asynchronous.Pool.open" "Return to general algorithm"
|
|
22
22
|
|
|
23
23
|
%% === Data objects ===
|
|
24
24
|
D_DSN[[DSN node list]]
|
|
@@ -17,14 +17,14 @@ DONE([Pool closed]):::neutral
|
|
|
17
17
|
|
|
18
18
|
CFG[/"Pool settings<br>· DSN<br>· pool size<br>· forbidden tiers<br>· cluster discovery mode"/]
|
|
19
19
|
|
|
20
|
-
START --> CFG --> CREATE["Pool.__init__"] --> CONNECT["pool.
|
|
20
|
+
START --> CFG --> CREATE["Pool.__init__"] --> CONNECT["pool.open()"] --> UsePool
|
|
21
21
|
CONNECT <-. write .-> POOL
|
|
22
22
|
|
|
23
|
-
CONNECT_DETAILS_NOTE>"
|
|
24
|
-
click CONNECT_DETAILS_NOTE "#picopyn.asynchronous.Pool.
|
|
23
|
+
CONNECT_DETAILS_NOTE>"Open algorithm details"]:::note
|
|
24
|
+
click CONNECT_DETAILS_NOTE "#picopyn.asynchronous.Pool.open" "Open algorithm details"
|
|
25
25
|
CONNECT_DETAILS_NOTE -.-> CONNECT
|
|
26
26
|
|
|
27
|
-
RECONCILE_NOTE>"
|
|
27
|
+
RECONCILE_NOTE>"pool membership changes<br>in the background:<br>connections are opened/closed<br>to match topology changes<br>(discovery mode: any cluster node,<br>bootstrap mode: DSN nodes only)"]:::note
|
|
28
28
|
click RECONCILE_NOTE "#picopyn.asynchronous.Pool.topology" "Reconcile algorithm details"
|
|
29
29
|
RECONCILE_NOTE -.-> POOL
|
|
30
30
|
|
|
@@ -15,8 +15,8 @@ INIT --> CACHE_EMPTY["Create empty query meta cache"] --> CONNECT["Fill the pool
|
|
|
15
15
|
NOTE_CONN>"connection options require<br>statement invalidation signal only"]:::note
|
|
16
16
|
NOTE_CONN -.-> CONNECT
|
|
17
17
|
|
|
18
|
-
CONNECT_DETAILS_NOTE>"
|
|
19
|
-
click CONNECT_DETAILS_NOTE "#picopyn.asynchronous.Pool.
|
|
18
|
+
CONNECT_DETAILS_NOTE>"Open algorithm details"]:::note
|
|
19
|
+
click CONNECT_DETAILS_NOTE "#picopyn.asynchronous.Pool.open" "Open algorithm details"
|
|
20
20
|
CONNECT_DETAILS_NOTE -.-> CONNECT
|
|
21
21
|
|
|
22
22
|
subgraph Pool1["Pool"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
```mermaid
|
|
2
|
+
---
|
|
3
|
+
title: Rebalance pool after reconcile
|
|
4
|
+
---
|
|
5
|
+
flowchart TD
|
|
6
|
+
|
|
7
|
+
classDef neutral fill:#f5f5f5,stroke:#9e9e9e,color:#424242
|
|
8
|
+
classDef note fill:#fffde7,stroke:#f9a825,stroke-dasharray:4,color:#555
|
|
9
|
+
classDef success fill:#e8f5e9,stroke:#4caf50,color:#1b5e20
|
|
10
|
+
|
|
11
|
+
START(["START"]):::neutral --> COUNT
|
|
12
|
+
|
|
13
|
+
COUNT_DETAILS_NOTE>"connections already marked<br>to evict are not counted:<br>they are closed on<br>release and refilled elsewhere"]:::note
|
|
14
|
+
COUNT_DETAILS_NOTE -.-> COUNT
|
|
15
|
+
|
|
16
|
+
COUNT_DETAILS_NOTE2>"candidate nodes are<br>online Picodata nodes<br>from allowed tiers"]:::note
|
|
17
|
+
COUNT_DETAILS_NOTE2 -.-> COUNT
|
|
18
|
+
|
|
19
|
+
COUNT["count free and in-use pool<br>connections per candidate uuid"] --> GAP
|
|
20
|
+
|
|
21
|
+
GAP_DETAILS_NOTE>"a gap of one<br>is not an imbalance"]:::note
|
|
22
|
+
GAP_DETAILS_NOTE -.-> GAP
|
|
23
|
+
|
|
24
|
+
GAP{"gap between<br>max count and min count<br>(most overloaded and<br>most underloaded nodes)<br>>= 2?"}
|
|
25
|
+
GAP -->|no| POOL_OK(["pool is balanced<br>(exit)"]):::success
|
|
26
|
+
GAP -->|yes| FREE{"is there a free connection<br>to the overloaded uuid?"}
|
|
27
|
+
|
|
28
|
+
FREE -->|no| EVICT_USED["mark one in-use connection<br>to the overloaded uuid to evict"]
|
|
29
|
+
FREE -->|yes| MIGRATE["migrate one connection:<br>take the free one out, connect<br>to the underloaded uuid,<br>close the old one"]
|
|
30
|
+
|
|
31
|
+
EVICT_DETAILS_NOTE>"release closes it, and the next<br>refresh refills the free slot to<br>the underloaded uuid"]:::note
|
|
32
|
+
EVICT_DETAILS_NOTE -.-> EVICT_USED
|
|
33
|
+
|
|
34
|
+
EVICT_USED --> POOL_OK3(["(exit)"]):::success
|
|
35
|
+
|
|
36
|
+
MIGRATE -->|migrated| LIMIT
|
|
37
|
+
MIGRATE -->|"could not connect to<br>the underloaded uuid"| EXCLUDE["put the connection back and<br>exclude the underloaded uuid<br>from the candidate list<br>in this reconcile"]
|
|
38
|
+
EXCLUDE --> LIMIT
|
|
39
|
+
|
|
40
|
+
LIMIT{"migration limit for<br>this reconcile reached?"}
|
|
41
|
+
LIMIT -->|no| GAP
|
|
42
|
+
LIMIT -->|yes| POOL_OK4(["(exit)"]):::success
|
|
43
|
+
|
|
44
|
+
LIMIT_DETAILS_NOTE>"the limit is proportional to<br>the pool size, see<br>Pool.rebalance_pool_divisor"]:::note
|
|
45
|
+
click LIMIT_DETAILS_NOTE "#picopyn.asynchronous.Pool" "Pool.rebalance_pool_divisor"
|
|
46
|
+
LIMIT_DETAILS_NOTE -.-> LIMIT
|
|
47
|
+
|
|
48
|
+
LIMIT_DETAILS_NOTE2>"the rest is left to<br>the next refresh"]:::note
|
|
49
|
+
LIMIT_DETAILS_NOTE2 -.-> POOL_OK4
|
|
50
|
+
```
|
|
@@ -10,7 +10,6 @@ classDef success fill:#e8f5e9,stroke:#4caf50,color:#1b5e20
|
|
|
10
10
|
classDef error fill:#ffebee,stroke:#f44336,color:#b71c1c
|
|
11
11
|
|
|
12
12
|
subgraph Pool["Pool"]
|
|
13
|
-
Settings[/"forbidden tiers=[...]<br>max size=N"/]
|
|
14
13
|
FreeConns["free conns=[...]"]
|
|
15
14
|
UsedConns["used conns=[...]"]
|
|
16
15
|
Topology[/"Topology"/]
|
|
@@ -25,14 +24,16 @@ TOPOLOGY_DETAILS_NOTE>"Topology details"]:::note
|
|
|
25
24
|
click TOPOLOGY_DETAILS_NOTE "../topology/#picopyn.topology.Topology" "Topology details"
|
|
26
25
|
TOPOLOGY_DETAILS_NOTE -.-> TOPOLOGY_APPLY
|
|
27
26
|
|
|
28
|
-
TOPOLOGY_APPLY -->
|
|
27
|
+
TOPOLOGY_APPLY --> COMPLETE{"topology has both<br>instances and replicasets?"}
|
|
28
|
+
COMPLETE -->|no| NOT_TRUSTED(["snapshot is not trustworthy:<br>pool is left as is<br>(exit)"]):::error
|
|
29
|
+
COMPLETE -->|yes| GET_LIST["build candidate_list from Topology:<br>- uuids with allowed tiers<br>- online uuids<br>- (bootstrap mode) only<br>DSN node addresses"]
|
|
29
30
|
GET_LIST --> REMOVE["close all connections whose<br>uuid not in candidate_list"] --> CHECK_SIZE_START
|
|
30
31
|
|
|
31
32
|
REMOVE -.-> FreeConns
|
|
32
33
|
REMOVE -.-> UsedConns
|
|
33
34
|
|
|
34
35
|
CHECK_SIZE_START{"current pool size < max size?"}
|
|
35
|
-
CHECK_SIZE_START -->|no|
|
|
36
|
+
CHECK_SIZE_START -->|no| REBALANCE
|
|
36
37
|
CHECK_SIZE_START -->|yes| MIN["choose uuid from candidate_list<br>with min connections count"]
|
|
37
38
|
|
|
38
39
|
MIN -->|got candidate| ADD_CONN["try to connect to candidate<br>and add connection to pool..."]
|
|
@@ -41,6 +42,13 @@ MIN -->|no candidate| NO_CANDIDATE2(["pool is temporarily not<br>full until next
|
|
|
41
42
|
ADD_CONN -->|connection failed| EXCLUDE["exclude the candidate uuid<br/>from list in this loop"] --> CHECK_SIZE_LOOP
|
|
42
43
|
ADD_CONN-->|connected| CHECK_SIZE_LOOP{"current size < max size?"}
|
|
43
44
|
|
|
44
|
-
CHECK_SIZE_LOOP -->|no|
|
|
45
|
+
CHECK_SIZE_LOOP -->|no| REBALANCE
|
|
45
46
|
CHECK_SIZE_LOOP -->|yes| MIN
|
|
47
|
+
|
|
48
|
+
REBALANCE["rebalance the pool:<br>move connections from overloaded<br>nodes to underloaded ones"]
|
|
49
|
+
REBALANCE --> POOL_OK(["Pool okay<br>(exit)"]):::success
|
|
50
|
+
|
|
51
|
+
REBALANCE_DETAILS_NOTE>"Rebalance details"]:::note
|
|
52
|
+
click REBALANCE_DETAILS_NOTE "#picopyn.asynchronous.Pool.topology--rebalance" "Rebalance details"
|
|
53
|
+
REBALANCE_DETAILS_NOTE -.-> REBALANCE
|
|
46
54
|
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
```mermaid
|
|
2
|
+
---
|
|
3
|
+
title: (Rebalance) a pooled node returned to Online (pool is already full)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
flowchart TD
|
|
7
|
+
|
|
8
|
+
classDef offline fill:#ffebee,stroke:#f44336,color:#b71c1c
|
|
9
|
+
classDef online fill:#e8f5e9,stroke:#4caf50,color:#1b5e20
|
|
10
|
+
classDef neutral fill:#f5f5f5,stroke:#9e9e9e,color:#424242
|
|
11
|
+
|
|
12
|
+
classDef note fill:#fffde7,stroke:#f9a825,stroke-dasharray:4,color:#555
|
|
13
|
+
|
|
14
|
+
subgraph before["Before"]
|
|
15
|
+
subgraph Picodata_before["Picodata"]
|
|
16
|
+
p1[("ins 1")]:::online
|
|
17
|
+
p2[("ins 2")]:::online
|
|
18
|
+
p3[("ins 3")]:::offline
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
subgraph Pool_before["Pool: max_size=6"]
|
|
22
|
+
c1["conn x3"] --> p1
|
|
23
|
+
c2["conn x3"] --> p2
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
EVENT1[/"Event<br>Instance 3 back Online"/]:::neutral -.-> REBALANCE{{"reconcile pool"}}
|
|
27
|
+
before -.-> REBALANCE
|
|
28
|
+
NOTE>"Pool is full, so refill<br>does nothing.<br>Initial connection counts:<br>[3/3/0], gap=3<br>=> migrate: [2/3/1], gap=2<br>=> migrate: [2/2/2], gap=0<br>Pool rebalanced in 2 refreshes"]:::note
|
|
29
|
+
NOTE -.-> REBALANCE
|
|
30
|
+
REBALANCE --> after
|
|
31
|
+
|
|
32
|
+
subgraph after["After (2 refreshes)"]
|
|
33
|
+
subgraph Picodata_after["Picodata"]
|
|
34
|
+
p21[("ins 1")]:::online
|
|
35
|
+
p22[("ins 2")]:::online
|
|
36
|
+
p23[("ins 3")]:::online
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
subgraph Pool_after["Pool: max_size=6"]
|
|
40
|
+
c21["conn x2"] --> p21
|
|
41
|
+
c22["conn x2"] --> p22
|
|
42
|
+
c23["conn x2"] --> p23
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
```
|
|
@@ -12,7 +12,7 @@ click ADR_DETAILS_NOTE "https://git.picodata.io/core/picodata/-/blob/master/doc/
|
|
|
12
12
|
|
|
13
13
|
TOPOLOGY[[Topology<br>·instances<br>·replicasets<br>·buckets<br>·tiers]]
|
|
14
14
|
|
|
15
|
-
CONNECT["Pool.
|
|
15
|
+
CONNECT["Pool.open()"] --> CFG[/"Topology tracker settings<br>· update interval"/] --> START["start topology tracker"] --> INITIAL["initial refresh<br>(sync, imitate ReadyForQuery)"] --> WAIT
|
|
16
16
|
INITAL_DETAILS_NOTE>"Picodata doesn't send it yet,<br>but it's planned: Picodata will send<br>ReadyForQuery once the initial snapshot<br>is fully delivered -- meaning our<br>topology view is consistent"]:::note
|
|
17
17
|
|
|
18
18
|
INITAL_DETAILS_NOTE -.-> INITIAL
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Explain
|
|
2
|
+
|
|
3
|
+
::: picopyn.explain
|
|
4
|
+
options:
|
|
5
|
+
show_root_heading: false
|
|
6
|
+
show_root_toc_entry: false
|
|
7
|
+
heading_level: 1
|
|
8
|
+
members: []
|
|
9
|
+
|
|
10
|
+
## API Reference
|
|
11
|
+
|
|
12
|
+
::: picopyn.explain
|
|
13
|
+
options:
|
|
14
|
+
show_root_heading: false
|
|
15
|
+
show_root_toc_entry: false
|
|
16
|
+
heading_level: 3
|
|
17
|
+
members_order: source
|
|
18
|
+
extra:
|
|
19
|
+
hide_docstring: true
|
|
20
|
+
filters:
|
|
21
|
+
- "!^_"
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
[](https://pypi.org/project/picopyn/)
|
|
2
|
+
[](https://pypi.org/project/picopyn)
|
|
3
|
+
|
|
1
4
|
# picopyn
|
|
2
5
|
|
|
3
6
|
**picopyn** is a Python connector for working with the distributed [Picodata](https://picodata.io/en/) database.
|
|
@@ -9,7 +12,7 @@
|
|
|
9
12
|
| >=25.2.1, <25.4.4 | 0.1.1 |
|
|
10
13
|
| >=25.4.4, <25.5.1 | 0.2.0 |
|
|
11
14
|
| >=25.5.1, <26.1.x | 1.0.0 |
|
|
12
|
-
| >=26.1.1, <26.2.x | 2.0.0
|
|
15
|
+
| >=26.1.1, <26.2.x | 2.0.0, 2.1.0 |
|
|
13
16
|
| 26.2.x | ⚠️ Unreleased yet |
|
|
14
17
|
|
|
15
18
|
## Features
|
|
@@ -19,8 +22,8 @@
|
|
|
19
22
|
* Pluggable load-balancing strategies
|
|
20
23
|
* Asynchronous API based on [asyncpg](https://github.com/MagicStack/asyncpg)
|
|
21
24
|
* (async only) [Shard-aware routing][picopyn.asynchronous.pool.Pool.execute]
|
|
22
|
-
* (async only) Automatic [pool membership reconciliation][picopyn.asynchronous.pool.Pool.topology] according to picodata topology
|
|
23
|
-
* Synchronous [
|
|
25
|
+
* (async only) Automatic [pool membership reconciliation][picopyn.asynchronous.pool.Pool.topology] according to picodata topology, including rebalancing a full pool across the nodes
|
|
26
|
+
* Synchronous [DB-API 2.0-compatible](https://peps.python.org/pep-0249/) API based on [psycopg](https://www.psycopg.org/)
|
|
24
27
|
|
|
25
28
|
## Quick start
|
|
26
29
|
|
|
@@ -32,22 +35,22 @@ Async:
|
|
|
32
35
|
|
|
33
36
|
```python
|
|
34
37
|
import asyncio
|
|
35
|
-
from picopyn.asynchronous import
|
|
38
|
+
from picopyn.asynchronous import Pool
|
|
36
39
|
|
|
37
40
|
async def main():
|
|
38
|
-
|
|
39
|
-
await
|
|
41
|
+
pool = Pool(dsn="postgresql://admin:pass@localhost:5432", enable_discovery=True)
|
|
42
|
+
await pool.open()
|
|
40
43
|
|
|
41
|
-
await
|
|
44
|
+
await pool.execute('''
|
|
42
45
|
CREATE TABLE "warehouse" (id INTEGER NOT NULL, item TEXT NOT NULL, PRIMARY KEY (id))
|
|
43
46
|
USING memtx DISTRIBUTED BY (id) OPTION (TIMEOUT = 3.0);
|
|
44
47
|
''')
|
|
45
48
|
|
|
46
|
-
await
|
|
47
|
-
rows = await
|
|
49
|
+
await pool.execute('INSERT INTO "warehouse" VALUES ($1::int, $2::varchar)', 1, "test")
|
|
50
|
+
rows = await pool.fetch('SELECT * FROM "warehouse"')
|
|
48
51
|
print(rows)
|
|
49
52
|
|
|
50
|
-
await
|
|
53
|
+
await pool.close()
|
|
51
54
|
|
|
52
55
|
asyncio.run(main())
|
|
53
56
|
```
|
|
@@ -2,6 +2,7 @@ from collections.abc import Callable
|
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
4
|
import asyncpg
|
|
5
|
+
from typing_extensions import deprecated
|
|
5
6
|
|
|
6
7
|
from picopyn.explain import ExplainPlan, ExplainRawPlan
|
|
7
8
|
|
|
@@ -9,9 +10,16 @@ from .connection import Connection
|
|
|
9
10
|
from .pool import Pool
|
|
10
11
|
|
|
11
12
|
|
|
13
|
+
@deprecated(
|
|
14
|
+
"picopyn.asynchronous.Client is deprecated and will be removed in the next major release, use picopyn.asynchronous.Pool instead"
|
|
15
|
+
)
|
|
12
16
|
class Client:
|
|
13
17
|
"""Async client for managing connections to a picodata cluster using a connection pool.
|
|
14
18
|
|
|
19
|
+
Deprecated:
|
|
20
|
+
This class will be removed in the next major release.
|
|
21
|
+
Use [picopyn.asynchronous.Pool][picopyn.asynchronous.Pool] instead.
|
|
22
|
+
|
|
15
23
|
This client handles connection pooling, automatic cluster node discovery,
|
|
16
24
|
and supports load balancing strategies for query distribution. See details about [Pool][picopyn.asynchronous.Pool].
|
|
17
25
|
|
|
@@ -94,7 +102,7 @@ class Client:
|
|
|
94
102
|
await client.connect()
|
|
95
103
|
```
|
|
96
104
|
"""
|
|
97
|
-
await self._pool.
|
|
105
|
+
await self._pool.open()
|
|
98
106
|
|
|
99
107
|
async def execute(self, query: str, *args: Any) -> str:
|
|
100
108
|
"""Executes a query that does not return rows (e.g. INSERT, UPDATE, DELETE).
|
|
@@ -21,29 +21,54 @@ from picopyn.query_metadata import (
|
|
|
21
21
|
parse_query_metadata_notice,
|
|
22
22
|
)
|
|
23
23
|
from picopyn.ssl import validate_ssl_source
|
|
24
|
-
from picopyn.utils import _redact_dsn
|
|
24
|
+
from picopyn.utils import _dsn_options, _redact_dsn
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def _with_picodata_startup_options(
|
|
28
|
-
connect_kwargs: dict[str, Any], *, request_query_metadata: bool
|
|
28
|
+
connect_kwargs: dict[str, Any], dsn: str, *, request_query_metadata: bool
|
|
29
29
|
) -> dict[str, Any]:
|
|
30
30
|
"""Append Picodata-specific startup options to `server_settings["options"]`,
|
|
31
|
-
without discarding options the caller already set.
|
|
31
|
+
without discarding options the caller already set in `connect_kwargs` or in the DSN.
|
|
32
|
+
|
|
33
|
+
Both `connect_kwargs` and the DSN can carry an `options` value. asyncpg merges the
|
|
34
|
+
DSN query params into `server_settings` and resolves the conflict per key in favor of
|
|
35
|
+
`connect_kwargs`, so a `server_settings["options"]` passed as a keyword argument
|
|
36
|
+
replaces the DSN one entirely.
|
|
37
|
+
|
|
38
|
+
In the example below `sql_vdbe_opcode_max` never reaches the server -- only the
|
|
39
|
+
Picodata options do:
|
|
40
|
+
```python
|
|
41
|
+
dsn = "postgresql://user:pass@host1:5432/db?options=sql_vdbe_opcode_max%3D1"
|
|
42
|
+
asyncpg.connect(dsn, server_settings={"options": picodata_options})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
So, to keep the caller's options and the driver functionality:
|
|
46
|
+
|
|
47
|
+
- if `connect_kwargs` already sets `server_settings["options"]`, the Picodata options
|
|
48
|
+
are appended to it -- the DSN `options` would be discarded by asyncpg anyway;
|
|
49
|
+
- otherwise the value is taken from the DSN, so it survives the keyword argument this
|
|
50
|
+
function builds;
|
|
51
|
+
- the Picodata options are appended last on purpose => even if the caller tries to turn
|
|
52
|
+
it off, the last value of a repeated option is kept.
|
|
32
53
|
|
|
33
54
|
Args:
|
|
34
|
-
connect_kwargs: Keyword arguments originally passed to
|
|
55
|
+
connect_kwargs: Keyword arguments originally passed to
|
|
56
|
+
[connect][picopyn.asynchronous.connection.Connection.connect].
|
|
57
|
+
dsn: The DSN the connection is about to be opened with; only its `options` query
|
|
58
|
+
parameter is read.
|
|
35
59
|
request_query_metadata: Whether to also [request query metadata][picopyn.query_metadata.QUERY_METADATA_OPTION],
|
|
36
|
-
in addition to [statements invalidation option][picopyn.query_metadata.STMT_INVALIDATION_OPTION]
|
|
60
|
+
in addition to [statements invalidation option][picopyn.query_metadata.STMT_INVALIDATION_OPTION]
|
|
61
|
+
(always requested).
|
|
37
62
|
|
|
38
63
|
Returns:
|
|
39
|
-
A copy of
|
|
64
|
+
A copy of `connect_kwargs` with the merged Picodata pgproto options.
|
|
40
65
|
"""
|
|
41
66
|
options = STMT_INVALIDATION_OPTION
|
|
42
67
|
if request_query_metadata:
|
|
43
68
|
options = f"{options},{QUERY_METADATA_OPTION}"
|
|
44
69
|
|
|
45
70
|
server_settings = dict(connect_kwargs.get("server_settings") or {})
|
|
46
|
-
existing_options = server_settings.get("options")
|
|
71
|
+
existing_options = server_settings.get("options") or _dsn_options(dsn)
|
|
47
72
|
server_settings["options"] = f"{existing_options},{options}" if existing_options else options
|
|
48
73
|
return {**connect_kwargs, "server_settings": server_settings}
|
|
49
74
|
|
|
@@ -120,7 +145,9 @@ class Connection:
|
|
|
120
145
|
await self.close()
|
|
121
146
|
|
|
122
147
|
connect_kwargs = _with_picodata_startup_options(
|
|
123
|
-
self._connect_kwargs,
|
|
148
|
+
self._connect_kwargs,
|
|
149
|
+
self._connect_dsn,
|
|
150
|
+
request_query_metadata=self._on_query_metadata is not None,
|
|
124
151
|
)
|
|
125
152
|
try:
|
|
126
153
|
self.conn = await asyncpg.connect(self._connect_dsn, **connect_kwargs)
|
|
@@ -315,9 +342,18 @@ class Connection:
|
|
|
315
342
|
Returns:
|
|
316
343
|
ExplainPlan for plain EXPLAIN or ExplainRawPlan for RAW mode.
|
|
317
344
|
|
|
345
|
+
Raises:
|
|
346
|
+
RuntimeError: If executing EXPLAIN fails or its output cannot be parsed. See
|
|
347
|
+
[`parse_plain_explain_lines()`][picopyn.explain.parse_plain_explain_lines]
|
|
348
|
+
and [`parse_raw_explain_lines()`][picopyn.explain.parse_raw_explain_lines]
|
|
349
|
+
for the underlying parsing functions and their errors.
|
|
350
|
+
|
|
318
351
|
Examples:
|
|
319
352
|
```python
|
|
320
|
-
plan = await conn.explain(
|
|
353
|
+
plan = await conn.explain(
|
|
354
|
+
'SELECT * FROM "warehouse" WHERE id = $1',
|
|
355
|
+
1,
|
|
356
|
+
)
|
|
321
357
|
raw_plan = await conn.explain("SELECT * FROM warehouse", raw=True)
|
|
322
358
|
```
|
|
323
359
|
"""
|