picopyn 2.0.0.dev1__tar.gz → 3.0.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.
Files changed (85) hide show
  1. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/AUTHORS.md +2 -1
  2. {picopyn-2.0.0.dev1/picopyn.egg-info → picopyn-3.0.0}/PKG-INFO +13 -13
  3. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/README.md +12 -12
  4. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/benchmark/bench.py +18 -15
  5. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/diagrams/_pool_init.md +1 -1
  6. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/diagrams/_pool_init_discovery_disabled.md +2 -2
  7. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/diagrams/_pool_init_discovery_enabled.md +3 -3
  8. picopyn-2.0.0.dev1/docs/diagrams/_pool_lifecycle.md → picopyn-3.0.0/docs/diagrams/_pool_lifecycle_async.md +7 -7
  9. picopyn-3.0.0/docs/diagrams/_pool_lifecycle_sync.md +44 -0
  10. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/diagrams/_query_metadata_pool_init.md +10 -9
  11. picopyn-3.0.0/docs/diagrams/_query_metadata_usage_lookup.md +37 -0
  12. picopyn-3.0.0/docs/diagrams/_rebalance_pool_after_reconcile.md +50 -0
  13. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/diagrams/_reconcile_pool_with_topology.md +12 -4
  14. picopyn-3.0.0/docs/diagrams/_reconcile_rebalance.md +45 -0
  15. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/diagrams/_request_if_missing.md +0 -2
  16. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/diagrams/_topology_components.md +2 -2
  17. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/diagrams/_topology_refresh.md +1 -1
  18. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/diagrams/_topology_tracking.md +2 -2
  19. picopyn-3.0.0/docs/explain.md +21 -0
  20. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/index.md +18 -11
  21. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/installation.md +1 -1
  22. picopyn-3.0.0/docs/sharding.md +7 -0
  23. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/sync.md +4 -4
  24. picopyn-3.0.0/docs/sync_internal.md +18 -0
  25. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn/__init__.py +3 -0
  26. picopyn-3.0.0/picopyn/asynchronous/__init__.py +9 -0
  27. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn/asynchronous/connection.py +45 -9
  28. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn/asynchronous/metadata_service.py +40 -8
  29. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn/asynchronous/pool.py +731 -193
  30. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn/asynchronous/topology_tracker.py +17 -72
  31. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn/bucket.py +116 -11
  32. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn/explain.py +159 -1
  33. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn/query_metadata.py +62 -37
  34. picopyn-3.0.0/picopyn/sharding.py +291 -0
  35. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn/synchronous/__init__.py +8 -3
  36. picopyn-3.0.0/picopyn/synchronous/connection.py +402 -0
  37. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn/synchronous/cursor.py +34 -0
  38. picopyn-3.0.0/picopyn/synchronous/metadata_service.py +272 -0
  39. picopyn-3.0.0/picopyn/synchronous/pool.py +1756 -0
  40. picopyn-3.0.0/picopyn/synchronous/topology_tracker.py +475 -0
  41. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn/topology.py +103 -39
  42. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn/utils/__init__.py +38 -5
  43. picopyn-3.0.0/picopyn/utils/balance.py +270 -0
  44. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn/utils/discovery.py +59 -3
  45. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn/utils/dsn.py +28 -0
  46. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn/utils/queries.py +34 -10
  47. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn/utils/topology.py +102 -10
  48. {picopyn-2.0.0.dev1 → picopyn-3.0.0/picopyn.egg-info}/PKG-INFO +13 -13
  49. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn.egg-info/SOURCES.txt +10 -3
  50. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/pyproject.toml +6 -3
  51. picopyn-2.0.0.dev1/docs/diagrams/_query_metadata_usage_lookup.md +0 -44
  52. picopyn-2.0.0.dev1/docs/explain.md +0 -11
  53. picopyn-2.0.0.dev1/picopyn/asynchronous/__init__.py +0 -11
  54. picopyn-2.0.0.dev1/picopyn/asynchronous/client.py +0 -180
  55. picopyn-2.0.0.dev1/picopyn/synchronous/client.py +0 -168
  56. picopyn-2.0.0.dev1/picopyn/synchronous/connection.py +0 -205
  57. picopyn-2.0.0.dev1/picopyn/synchronous/pool.py +0 -652
  58. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/LICENSE +0 -0
  59. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/MANIFEST.in +0 -0
  60. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/benchmark/README.md +0 -0
  61. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/benchmark/examples/10k_rows_1_thread_1_batch/overall_speed_time.png +0 -0
  62. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/benchmark/examples/10k_rows_1_thread_1_batch/threads_speed_time.png +0 -0
  63. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/benchmark/examples/1m_rows_10_threads_100_batch/errors_cumulative.png +0 -0
  64. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/benchmark/examples/1m_rows_10_threads_100_batch/overall_speed_time.png +0 -0
  65. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/benchmark/examples/1m_rows_10_threads_100_batch/threads_speed_time.png +0 -0
  66. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/benchmark/generate_data_to_csv.py +0 -0
  67. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/async.md +0 -0
  68. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/async_internal.md +0 -0
  69. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/bucket.md +0 -0
  70. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/changelog.md +0 -0
  71. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/contributing.md +0 -0
  72. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/diagrams/_reconcile_node_outside_pool_died.md +0 -0
  73. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/diagrams/_reconcile_pooled_node_died_size_eq.md +0 -0
  74. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/diagrams/_reconcile_pooled_node_died_size_gt.md +0 -0
  75. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/diagrams/_reconcile_pooled_node_died_size_lt.md +0 -0
  76. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/diagrams/_topology_apply_bucket_event.md +0 -0
  77. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/diagrams/_topology_apply_bucket_replicaset_event.md +0 -0
  78. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/query_metadata.md +0 -0
  79. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/docs/topology.md +0 -0
  80. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn/ssl.py +0 -0
  81. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn/synchronous/errors.py +0 -0
  82. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn.egg-info/dependency_links.txt +0 -0
  83. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn.egg-info/requires.txt +0 -0
  84. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/picopyn.egg-info/top_level.txt +0 -0
  85. {picopyn-2.0.0.dev1 → picopyn-3.0.0}/setup.cfg +0 -0
@@ -2,4 +2,5 @@
2
2
 
3
3
  - Asya Lomakina <a.lomakina@picodata.io>
4
4
  - Igor Kuznetsov <i.kuznetsov@picodata.io>
5
- - Dmitriy Koltsov <dkoltsov@picodata.io>
5
+ - Dmitriy Koltsov <dkoltsov@picodata.io>
6
+ - Daniil Gorlyakov <d.gorlyakov@picodata.io>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: picopyn
3
- Version: 2.0.0.dev1
3
+ Version: 3.0.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,17 +55,17 @@ 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, <4.0.0 |
59
59
  | 26.2.x | ⚠️ Unreleased yet |
60
60
 
61
61
  ## Features
62
62
  * Connection pooling with configurable pool size
63
63
  * Optional automatic node discovery
64
64
  * Pluggable load-balancing strategies
65
+ * Shard-aware routing
66
+ * Automatic pool membership reconciliation, including rebalancing a full pool across the nodes
65
67
  * Asynchronous API based on [asyncpg](https://github.com/MagicStack/asyncpg)
66
- * (async only) Shard-aware routing
67
- * (async only) Automatic pool membership reconciliation
68
- * Synchronous [DPAPI-compatible](https://peps.python.org/pep-0249/) API based on [psycopg](https://www.psycopg.org/)
68
+ * Synchronous [DB-API 2.0-compatible](https://peps.python.org/pep-0249/) API based on [psycopg](https://www.psycopg.org/)
69
69
 
70
70
  ## Navigation
71
71
  * [Installation](#installation)
@@ -97,24 +97,24 @@ Async:
97
97
 
98
98
  ```python
99
99
  import asyncio
100
- from picopyn.asynchronous import Client
100
+ from picopyn.asynchronous import Pool
101
101
 
102
102
  async def main():
103
- # create and connect client to the picodata cluster
104
- client = Client(dsn="postgresql://admin:pass@localhost:5432")
105
- await client.connect()
103
+ # create and open pool to the picodata cluster
104
+ pool = Pool(dsn="postgresql://admin:pass@localhost:5432", enable_discovery=True)
105
+ await pool.open()
106
106
 
107
107
  # execute DDL operations
108
- await client.execute('''
108
+ await pool.execute('''
109
109
  CREATE TABLE "warehouse" (id INTEGER NOT NULL, item TEXT NOT NULL, PRIMARY KEY (id)) USING memtx DISTRIBUTED BY (id) OPTION (TIMEOUT = 3.0);
110
110
  ''')
111
111
 
112
112
  # execute DML/DQL operations
113
- await client.execute('INSERT INTO "warehouse" VALUES ($1::int, $2::varchar)', 1, "test")
114
- rows = await client.fetch('SELECT * FROM "warehouse"')
113
+ await pool.execute('INSERT INTO "warehouse" VALUES ($1::int, $2::varchar)', 1, "test")
114
+ rows = await pool.fetch('SELECT * FROM "warehouse"')
115
115
  print(rows)
116
116
 
117
- await client.close()
117
+ await pool.close()
118
118
 
119
119
  asyncio.run(main())
120
120
  ```
@@ -12,17 +12,17 @@
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, <4.0.0 |
16
16
  | 26.2.x | ⚠️ Unreleased yet |
17
17
 
18
18
  ## Features
19
19
  * Connection pooling with configurable pool size
20
20
  * Optional automatic node discovery
21
21
  * Pluggable load-balancing strategies
22
+ * Shard-aware routing
23
+ * Automatic pool membership reconciliation, including rebalancing a full pool across the nodes
22
24
  * Asynchronous API based on [asyncpg](https://github.com/MagicStack/asyncpg)
23
- * (async only) Shard-aware routing
24
- * (async only) Automatic pool membership reconciliation
25
- * Synchronous [DPAPI-compatible](https://peps.python.org/pep-0249/) API based on [psycopg](https://www.psycopg.org/)
25
+ * Synchronous [DB-API 2.0-compatible](https://peps.python.org/pep-0249/) API based on [psycopg](https://www.psycopg.org/)
26
26
 
27
27
  ## Navigation
28
28
  * [Installation](#installation)
@@ -54,24 +54,24 @@ Async:
54
54
 
55
55
  ```python
56
56
  import asyncio
57
- from picopyn.asynchronous import Client
57
+ from picopyn.asynchronous import Pool
58
58
 
59
59
  async def main():
60
- # create and connect client to the picodata cluster
61
- client = Client(dsn="postgresql://admin:pass@localhost:5432")
62
- await client.connect()
60
+ # create and open pool to the picodata cluster
61
+ pool = Pool(dsn="postgresql://admin:pass@localhost:5432", enable_discovery=True)
62
+ await pool.open()
63
63
 
64
64
  # execute DDL operations
65
- await client.execute('''
65
+ await pool.execute('''
66
66
  CREATE TABLE "warehouse" (id INTEGER NOT NULL, item TEXT NOT NULL, PRIMARY KEY (id)) USING memtx DISTRIBUTED BY (id) OPTION (TIMEOUT = 3.0);
67
67
  ''')
68
68
 
69
69
  # execute DML/DQL operations
70
- await client.execute('INSERT INTO "warehouse" VALUES ($1::int, $2::varchar)', 1, "test")
71
- rows = await client.fetch('SELECT * FROM "warehouse"')
70
+ await pool.execute('INSERT INTO "warehouse" VALUES ($1::int, $2::varchar)', 1, "test")
71
+ rows = await pool.fetch('SELECT * FROM "warehouse"')
72
72
  print(rows)
73
73
 
74
- await client.close()
74
+ await pool.close()
75
75
 
76
76
  asyncio.run(main())
77
77
  ```
@@ -8,7 +8,7 @@ from uuid import UUID
8
8
  import matplotlib
9
9
  import numpy as np
10
10
 
11
- from picopyn.asynchronous import Client
11
+ from picopyn.asynchronous import Pool
12
12
 
13
13
  matplotlib.use("Agg")
14
14
  import os
@@ -121,17 +121,17 @@ def read_csv(input_file):
121
121
  exit(1)
122
122
 
123
123
 
124
- async def prepare_database(client):
124
+ async def prepare_database(pool):
125
125
  """
126
126
  Prepare the database: drop and recreate the target table.
127
127
 
128
128
  Args:
129
- client (Client): Picodata client connection.
129
+ pool (Pool): Picodata connection pool.
130
130
  """
131
131
  try:
132
- await client.connect()
133
- await client.execute("DROP TABLE IF EXISTS test_all_types;")
134
- await client.execute(
132
+ await pool.open()
133
+ await pool.execute("DROP TABLE IF EXISTS test_all_types;")
134
+ await pool.execute(
135
135
  """
136
136
  CREATE TABLE test_all_types(
137
137
  id INTEGER PRIMARY KEY,
@@ -176,13 +176,13 @@ def build_batch_insert_sql(batch_size: int) -> str:
176
176
  )
177
177
 
178
178
 
179
- async def worker(thread_id, client, batches, durations, errors, start_time, args):
179
+ async def worker(thread_id, pool, batches, durations, errors, start_time, args):
180
180
  """
181
181
  Worker coroutine to perform batch inserts for assigned batches.
182
182
 
183
183
  Args:
184
184
  thread_id (int): Worker ID.
185
- client (Client): Picodata client connection.
185
+ pool (Pool): Picodata connection pool.
186
186
  batches (list): List of batch rows to insert.
187
187
  durations (list): List to collect durations per batch.
188
188
  errors (list): List to collect timestamps of failed inserts.
@@ -194,7 +194,7 @@ async def worker(thread_id, client, batches, durations, errors, start_time, args
194
194
  try:
195
195
  sql = build_batch_insert_sql(len(batch))
196
196
  flat_args = [item for row in batch for item in row]
197
- await client.execute(sql, *flat_args)
197
+ await pool.execute(sql, *flat_args)
198
198
  except Exception as e:
199
199
  if args.log_insert_errors:
200
200
  print(f"[ERROR] Failed batch insert: {e}")
@@ -203,12 +203,12 @@ async def worker(thread_id, client, batches, durations, errors, start_time, args
203
203
  durations[thread_id].append(end - start)
204
204
 
205
205
 
206
- async def run_benchmark(client, rows, args):
206
+ async def run_benchmark(pool, rows, args):
207
207
  """
208
208
  Run the entire insert benchmark using multiple workers.
209
209
 
210
210
  Args:
211
- client (Client): Picodata client.
211
+ pool (Pool): Picodata connection pool.
212
212
  rows (list): Parsed rows to insert.
213
213
  args (Namespace): Parsed arguments.
214
214
 
@@ -229,7 +229,7 @@ async def run_benchmark(client, rows, args):
229
229
  errors = []
230
230
 
231
231
  tasks = [
232
- worker(i, client, batches_per_thread[i], durations, errors, start_time, args)
232
+ worker(i, pool, batches_per_thread[i], durations, errors, start_time, args)
233
233
  for i in range(args.threads)
234
234
  ]
235
235
  await asyncio.gather(*tasks)
@@ -473,10 +473,13 @@ async def main():
473
473
 
474
474
  rows = read_csv(args.input)
475
475
 
476
- client = Client(dsn=args.cluster, pool_size=args.threads)
477
- await prepare_database(client)
476
+ pool = Pool(dsn=args.cluster, max_size=args.threads, enable_discovery=True)
477
+ try:
478
+ await prepare_database(pool)
478
479
 
479
- durations, errors = await run_benchmark(client, rows, args)
480
+ durations, errors = await run_benchmark(pool, rows, args)
481
+ finally:
482
+ await pool.close()
480
483
 
481
484
  build_thread_graph(durations, args)
482
485
  build_overall_graph(durations, rows, args)
@@ -16,7 +16,7 @@ ERROR([Initialization failed]):::error
16
16
  CFG[/"Pool settings<br>· DSN string<br>· pool size<br>· forbidden tiers<br>· cluster discovery mode"/]
17
17
 
18
18
  USAGE_DETAILS_NOTE>"Return to pool lifecycle details"]:::note
19
- click USAGE_DETAILS_NOTE "#picopyn.asynchronous.Pool" "Return to pool lifecycle details"
19
+ click USAGE_DETAILS_NOTE "#picopyn.$MOD$.Pool" "Return to pool lifecycle details"
20
20
 
21
21
  %% === Data objects ===
22
22
  POOL[[Connection pool]]
@@ -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.connect" "Return to general algorithm"
20
+ click GENERAL_DETAILS_NOTE "#picopyn.$MOD$.Pool.open" "Return to general algorithm"
21
21
 
22
22
  %% === Data objects ===
23
23
  B_DSN[[DSN node list]]
@@ -29,7 +29,7 @@ START --> CFG --> PARSE[Parse DSN string to DSN node list] --> B_PoolFilling
29
29
  PARSE <-. write .-> B_DSN
30
30
  B_DSN <-. read .-> B_CONNECT_NODE[Connect to DSN node]
31
31
 
32
- B_PoolFilling_NOTE>"iterate over DSN nodes until either:<br>- the list is empty<br>- pool_size connections<br>have been established"]:::note
32
+ B_PoolFilling_NOTE>"iterate over DSN nodes until either:<br>- the list is empty<br>- max size connections<br>have been established"]:::note
33
33
  B_PoolFilling_NOTE -.-> B_PoolFilling
34
34
  subgraph B_PoolFilling["Pool filling"]
35
35
  direction TB
@@ -15,10 +15,10 @@ START([Start]):::neutral
15
15
  DONE([Pool ready]):::success
16
16
  D_ERROR_CAND([Initialization failed]):::error
17
17
  ERROR([Initialization failed]):::error
18
- CFG[/"Pool settings<br>· DSN string<br>· pool_size<br>· forbidden tiers<br>· cluster discovery enabled"/]
18
+ CFG[/"Pool settings<br>· DSN string<br>· max_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.connect" "Return to general algorithm"
21
+ click GENERAL_DETAILS_NOTE "#picopyn.$MOD$.Pool.open" "Return to general algorithm"
22
22
 
23
23
  %% === Data objects ===
24
24
  D_DSN[[DSN node list]]
@@ -40,7 +40,7 @@ subgraph DiscoverNodes["Discover cluster nodes"]
40
40
  end
41
41
  DiscoverNodes --> D_PoolFilling
42
42
 
43
- D_PoolFilling_NOTE>"iterate over candidate nodes<br>until either:<br>- the list is empty<br>- pool_size connections<br>have been established"]:::note
43
+ D_PoolFilling_NOTE>"iterate over candidate nodes<br>until either:<br>- the list is empty<br>- max_size connections<br>have been established"]:::note
44
44
  D_PoolFilling_NOTE -.-> D_PoolFilling
45
45
  CAND <-. read .-> D_CONNECT_NODE[Connect to candidate node]
46
46
  subgraph D_PoolFilling["Pool filling"]
@@ -17,15 +17,15 @@ 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.connect()"] --> UsePool
20
+ START --> CFG --> CREATE["Pool.__init__"] --> CONNECT["pool.open()"] --> UsePool
21
21
  CONNECT <-. write .-> POOL
22
22
 
23
- CONNECT_DETAILS_NOTE>"Connect algorithm details"]:::note
24
- click CONNECT_DETAILS_NOTE "#picopyn.asynchronous.Pool.connect" "Connect algorithm details"
23
+ CONNECT_DETAILS_NOTE>"Open algorithm details"]:::note
24
+ click CONNECT_DETAILS_NOTE "#picopyn.$MOD$.Pool.open" "Open algorithm details"
25
25
  CONNECT_DETAILS_NOTE -.-> CONNECT
26
26
 
27
- RECONCILE_NOTE>"(discovery mode only):<br>pool membership changes<br>in the background:<br>connections are opened/closed<br>to match topology changes"]:::note
28
- click RECONCILE_NOTE "#picopyn.asynchronous.Pool.topology" "Reconcile algorithm details"
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
+ click RECONCILE_NOTE "#picopyn.$MOD$.Pool.topology" "Reconcile algorithm details"
29
29
  RECONCILE_NOTE -.-> POOL
30
30
 
31
31
  CYCLE_NOTE>"repeated for each<br>database operation"]:::note
@@ -33,12 +33,12 @@ CYCLE_NOTE -.-> UsePool
33
33
 
34
34
  subgraph UsePool["Pool usage"]
35
35
  direction TB
36
- ACQUIRE["pool.acquire()"] --> USE["Execute queries<br>· pool.fetchrow()<br>· pool.fetch()<br>· pool.execute()"] --> RELEASE["pool.release(conn)"]
36
+ ACQUIRE["pool.acquire() -> conn"] --> USE["Execute queries<br>· conn.fetchrow()<br>· conn.fetch()<br>· conn.execute()"] --> RELEASE["pool.release(conn)"]
37
37
  end
38
38
 
39
39
  ACQUIRE <-. delete .-> POOL
40
40
  RELEASE <-. write .-> POOL
41
- CLOSE <-. delete .-> POOL
41
+ CLOSE["pool.close()"] <-. delete .-> POOL
42
42
  UsePool --> CLOSE
43
43
  CLOSE --> DONE
44
44
  ```
@@ -0,0 +1,44 @@
1
+ ```mermaid
2
+ ---
3
+ title: Pool lifecycle
4
+ ---
5
+
6
+ flowchart TD
7
+ %% === Style ===
8
+ classDef neutral fill:#f5f5f5,stroke:#9e9e9e,color:#424242
9
+ classDef note fill:#fffde7,stroke:#f9a825,stroke-dasharray:4,color:#555
10
+
11
+ %% === Data objects ===
12
+ POOL[[Connection pool]]
13
+
14
+ %% === Lifecycle ===
15
+ START([Start]):::neutral
16
+ DONE([Pool closed]):::neutral
17
+
18
+ CFG[/"Pool settings<br>· DSN<br>· pool size<br>· forbidden tiers<br>· cluster discovery mode"/]
19
+
20
+ START --> CFG --> CREATE["Pool.__init__"] --> CONNECT["pool.open()"] --> UsePool
21
+ CONNECT <-. write .-> POOL
22
+
23
+ CONNECT_DETAILS_NOTE>"Open algorithm details"]:::note
24
+ click CONNECT_DETAILS_NOTE "#picopyn.$MOD$.Pool.open" "Open algorithm details"
25
+ CONNECT_DETAILS_NOTE -.-> CONNECT
26
+
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
+ click RECONCILE_NOTE "#picopyn.$MOD$.Pool.topology" "Reconcile algorithm details"
29
+ RECONCILE_NOTE -.-> POOL
30
+
31
+ CYCLE_NOTE>"repeated for each<br>database operation"]:::note
32
+ CYCLE_NOTE -.-> UsePool
33
+
34
+ subgraph UsePool["Pool usage"]
35
+ direction TB
36
+ ACQUIRE["with pool.connection() as conn"] --> CURSOR["conn.cursor() -> cursor"] --> USE["Execute queries<br>· cursor.fetchone()<br>· cursor.execute()<br>· ..."] --> RELEASE["leaving the with block<br>releases the connection"]
37
+ end
38
+
39
+ ACQUIRE <-. delete .-> POOL
40
+ RELEASE <-. write .-> POOL
41
+ CLOSE["pool.close()"] <-. delete .-> POOL
42
+ UsePool --> CLOSE
43
+ CLOSE --> DONE
44
+ ```
@@ -10,13 +10,13 @@ classDef note fill:#fffde7,stroke:#f9a825,stroke-dasharray:4,color:#555
10
10
 
11
11
  INIT(["Pool init"]):::neutral
12
12
 
13
- INIT --> CACHE_EMPTY["Create empty query meta cache"] --> CONNECT["Fill the pool with ordinary<br>connections to picodata"] --> Pool1
13
+ INIT --> CONNECT["Fill the pool with ordinary<br>connections to picodata"] --> Pool1
14
14
 
15
- NOTE_CONN>"connection options require<br>statement invalidation signal only"]:::note
15
+ NOTE_CONN>"connection options require<br>statement invalidation<br>signal only"]:::note
16
16
  NOTE_CONN -.-> CONNECT
17
17
 
18
- CONNECT_DETAILS_NOTE>"Connect algorithm details"]:::note
19
- click CONNECT_DETAILS_NOTE "#picopyn.asynchronous.Pool.connect" "Connect algorithm details"
18
+ CONNECT_DETAILS_NOTE>"Open algorithm details"]:::note
19
+ click CONNECT_DETAILS_NOTE "#picopyn.$MOD$.Pool.open" "Open algorithm details"
20
20
  CONNECT_DETAILS_NOTE -.-> CONNECT
21
21
 
22
22
  subgraph Pool1["Pool"]
@@ -25,13 +25,14 @@ subgraph Pool1["Pool"]
25
25
  CONN11["conn to inst1"] --> PICO11[(Picodata)]
26
26
  CONN12["..."] --> PICO12[(Picodata)]
27
27
  CONN1N["conn to instN"] --> PICO1N[(Picodata)]
28
-
29
- CACHE1[/"Query meta cache"/]
30
28
  end
31
29
 
32
- Pool1 --> CONNECT_SERV["create service connection<br>for meta listener"]
30
+ Pool1 --> CONNECT_SERV["create empty query meta cache<br>and service connection<br>for meta listener"]
31
+
32
+ NOTE_DISABLED>"skipped entirely when<br>service disabled: no cache,<br>no service connection"]:::note
33
+ NOTE_DISABLED -.-> CONNECT_SERV
33
34
 
34
- NOTE_CONN_SERV>"connection options require statement invalidation signal<br>and distribution key metadata"]:::note
35
+ NOTE_CONN_SERV>"connection options require<br>statement invalidation signal<br>and distribution key metadata"]:::note
35
36
  NOTE_CONN_SERV -.-> CONNECT_SERV
36
37
 
37
38
  CONNECT_SERV --> LIST["add picodata notice listener"]
@@ -45,7 +46,7 @@ subgraph Pool["Pool"]
45
46
  CONN2["..."] --> PICO2[(...)]
46
47
  CONNN["connN for<br>user usage"] --> PICON[(PicodataN)]
47
48
 
48
- PIC(["can be any of"]) -.- CONNSERV["service connection with Notice listener"]
49
+ PIC(["can be any of"]) -.- CONNSERV["service connection<br>with Notice listener"]
49
50
  PICO1 -.- PIC
50
51
  PICO2 -.- PIC
51
52
  PICON -.- PIC
@@ -0,0 +1,37 @@
1
+ ```mermaid
2
+ ---
3
+ title: Query metadata -- usage lookup
4
+ ---
5
+
6
+ sequenceDiagram
7
+ participant App as Application
8
+ box rgba(128,128,128,0.15) Picopyn
9
+ participant Driver as Core
10
+ participant Pool as Connection pool
11
+ participant Meta as Service connection
12
+ end
13
+ participant Server as Picodata
14
+
15
+ App->>Driver: pool.execute(query, params)
16
+
17
+ alt First call: cache miss
18
+ Driver->>Pool: Any free connection
19
+ else Repeated call: cache hit
20
+ Driver->>Driver: query meta + params -> bucket_id -> node
21
+ Driver->>Pool: Connection to that node
22
+ end
23
+ Pool-->>Driver: Connection
24
+
25
+ Driver->>Server: Bind + Execute
26
+ Server-->>Driver: Result
27
+ Driver-->>App: Result
28
+
29
+ rect rgba(128,128,128,0.15)
30
+ Note over Driver,Server: In the background, only after a cache miss
31
+ Driver->>Meta: query
32
+ Meta->>Server: Parse(query)
33
+ Server-->>Meta: NoticeResponse
34
+ Meta-->>Driver: query meta
35
+ Driver->>Driver: LRU cache
36
+ end
37
+ ```
@@ -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.$MOD$.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 --> GET_LIST["build candidate_list from Topology:<br>- uuids with allowed tiers<br>- online uuids"]
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| POOL_OK(["Pool okay<br>(exit)"]):::success
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| POOL_OK2(["Pool okay<br>(exit)"]):::success
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.$MOD$.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
+ ```
@@ -9,10 +9,8 @@ classDef neutral fill:#f5f5f5,stroke:#9e9e9e,color:#424242
9
9
  classDef note fill:#fffde7,stroke:#f9a825,stroke-dasharray:4,color:#555
10
10
 
11
11
  QUERY(["parametrized query"]):::neutral
12
- HASH(["hash of the<br>query text"]):::neutral
13
12
 
14
13
  QUERY --> PARSE_QUERY
15
- HASH --> PARSE_QUERY
16
14
  PARSE_QUERY["ask Picodata to parse query"] -. Parse .-> PICO[("Picodata")] -. Notice message .-> GET
17
15
 
18
16
  GET["receive the notice"] --> PARSE_MSG{"is this query metadata?"}
@@ -20,10 +20,10 @@ flowchart TB
20
20
  click TOPOLOGY_DETAILS_NOTE "../topology/#picopyn.topology.Topology" "See topology details"
21
21
 
22
22
  TRACKER_DETAILS_NOTE>"See topology tracker details"]:::note -.-> TRACKER
23
- click TRACKER_DETAILS_NOTE "#picopyn.asynchronous.topology_tracker.TopologyTracker" "Topology tracker details"
23
+ click TRACKER_DETAILS_NOTE "#picopyn.$MOD$.topology_tracker.TopologyTracker" "Topology tracker details"
24
24
 
25
25
  SOURCE_DETAILS_NOTE>"See topology source details"]:::note -.-> SOURCE
26
- click SOURCE_DETAILS_NOTE "#picopyn.asynchronous.topology_tracker.PollingTopologySource" "Topology source details"
26
+ click SOURCE_DETAILS_NOTE "#picopyn.$MOD$.topology_tracker.PollingTopologySource" "Topology source details"
27
27
 
28
28
  NOTE>"today: events are built by driver<br>polled Picodata's system<br>tables/functions.<br>planned: Picodata itself<br>is the source, pushing<br>events directly"]:::note
29
29
  NOTE -.-> SOURCE
@@ -44,7 +44,7 @@ BUCKETS_DETAILS_NOTE>"Bucket event details"]:::note
44
44
  click BUCKETS_DETAILS_NOTE "#picopyn.topology.Topology.apply_bucket" "Bucket event details"
45
45
  BUCKETS_DETAILS_NOTE -.-> BUCKET_APPLY
46
46
 
47
- NOTE>"_pico_bucket(tier) may not exist<br>on older Picodata -- failure is logged<br>and skipped per tier, not fatal"]:::note
47
+ NOTE>"_pico_bucket(tier) may not exist<br>on older Picodata -- failure<br>is logged and skipped per tier,<br>not fatal"]:::note
48
48
  NOTE -.-> BUCKET_FETCH
49
49
  BUCKET_APPLY <-. write .-> TOPOLOGY4[/Topology/]
50
50
 
@@ -12,8 +12,8 @@ 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.connect()"] --> CFG[/"Topology tracker settings<br>· update interval"/] --> START["start topology tracker"] --> INITIAL["initial refresh<br>(sync, imitate ReadyForQuery)"] --> WAIT
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
15
+ CONNECT["Pool.open()"] --> CFG[/"Topology tracker settings<br>· update interval"/] --> START["start topology tracker"] --> INITIAL["initial refresh<br>(sync, imitate ReadyForQuery)"] --> WAIT
16
+ INITAL_DETAILS_NOTE>"Picodata doesn't send it yet,<br>but it's planned: Picodata will<br>send ReadyForQuery once the<br>initial snapshot is fully<br>delivered -- meaning our<br>topology view is consistent"]:::note
17
17
 
18
18
  INITAL_DETAILS_NOTE -.-> INITIAL
19
19
 
@@ -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
+ - "!^_"