Async: Internal
Warning
Not public API. Driver wires this up and owns it; nothing here is meant to be constructed or called directly. Documented anyway so you can see how it actually works
picopyn.asynchronous.topology_tracker
Keeps a Topology object up to date, using a swappable source.
Three pieces, three narrow jobs:
- Topology: only consumes events and applies them consistently. It doesn't know or care where events come from.
- TopologyTracker:
listens to a
TopologySourceand applies whatever events it gets toTopology. It doesn't know or care how events are produced. TopologySource: produces events and hands them to the tracker. Currently PollingTopologySource builds them by polling Picodata's system tables/functions. Later, once Picodata starts pushing topology changes as NOTICE messages (follow the issue),NotifyTopologySourcewill replace it -- at that point Picodata itself effectively becomes the source.
---
title: Topology components
---
flowchart TB
classDef neutral fill:#f5f5f5,stroke:#9e9e9e,color:#424242
classDef note fill:#fffde7,stroke:#f9a825,stroke-dasharray:4,color:#555
subgraph SOURCE["TopologySource"]
direction TB
POLL["PollingTopologySource<br>(today)"]:::neutral
NOTIFY["NotifyTopologySource<br>(planned)"]:::neutral
end
SOURCE -->|produces events| TRACKER["TopologyTracker<br>listens to a source,<br>applies events it gets"]
TRACKER -->|apply_instance /<br>apply_replicaset /<br>apply_bucket /<br>apply_tiers| TOPOLOGY[["Topology<br>only job: consume events<br>and apply them consistently"]]
TOPOLOGY_DETAILS_NOTE>"See topology details"]:::note -.-> TOPOLOGY
click TOPOLOGY_DETAILS_NOTE "../topology/#picopyn.topology.Topology" "See topology details"
TRACKER_DETAILS_NOTE>"See topology tracker details"]:::note -.-> TRACKER
click TRACKER_DETAILS_NOTE "#picopyn.asynchronous.topology_tracker.TopologyTracker" "Topology tracker details"
SOURCE_DETAILS_NOTE>"See topology source details"]:::note -.-> SOURCE
click SOURCE_DETAILS_NOTE "#picopyn.asynchronous.topology_tracker.PollingTopologySource" "Topology source details"
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
NOTE -.-> SOURCE
Note
The tracker only keeps Topology up to date, so other driver parts (e.g. Pool) can use it for pool membership changes or shard-aware routing. The tracker also accepts a refresh callback that runs right after each refresh -- the pool registers its reconcile there.
PollingTopologySource
Bases: TopologySource
Polls Picodata system tables/functions over one connection, opened on first use and reopened if it dies.
Should be replaced by real NOTIFY messages once Picodata supports them
(follow the issue).
Note
Scalar function used to poll bucket ranges, supported by Picodata >=25.5.1
Source code in picopyn/asynchronous/topology_tracker.py
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | |
refresh(topology)
async
Fetch the current topology and apply it, (re)connecting if needed.
Reconnects first if there's no live connection, clearing topology
in that case -- the new connection may be talking to an instance
that's behind or ahead of the one we had before. If reconnecting
fails, returns without touching topology.
Sequence of applying changes:
- replicaset events
- instance events
- tiers
Note
According to Picodata's topology notifications ADR, once Picodata has delivered every event of the current snapshot, it sends a ReadyForQuery message, telling us our Topology view is now consistent.
This matters because we need to be sure we've received every tier's info (via replicaset events) and its buckets (via bucket events) before trusting the snapshot. Picodata doesn't send ReadyForQuery yet, so we have no way of knowing when all tiers have shown up in replicaset events.
Fortunately, we can always get each tier's bucket count from the
_pico_tier
system table and trust it as is -- no extra checks needed. It's populated
with every tier and its bucket count
exactly once,
all rows at once. So all we need to do is read the table once and store it
in topology.
- bucket events (based on the tier list already known to the topology)
Note
The bucket map is only loosely coupled to the rest of the
topology (replicasets/instances). Failing to fetch buckets for a
tier does not mean the rest of the topology is wrong -- the
topology itself can be known and correct while its buckets are
temporarily not. This happens e.g. when a replicaset's weight
changes: replicasets/tiers/instances don't change and are known
right away, but bucket migration is still in progress, so
_pico_bucket(tier) responds with an error like "rebalance in
progress" until it settles.
This is expected to go away once we switch to NOTIFY-based
topology: Picodata will only send us an event once a migration
has actually completed, so there won't be an in-between state
to poll into an error -- we'll just apply the bucket map change
when told.
Source code in picopyn/asynchronous/topology_tracker.py
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | |
TopologySource
Bases: ABC
Something that can bring a Topology up to date.
Source code in picopyn/asynchronous/topology_tracker.py
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | |
close()
abstractmethod
async
Release any resources (e.g. connections) this source holds open.
Source code in picopyn/asynchronous/topology_tracker.py
66 67 68 69 | |
terminate()
abstractmethod
Terminate any resources this source holds open without graceful close.
Source code in picopyn/asynchronous/topology_tracker.py
71 72 73 74 | |
TopologyTracker
Holds a topology view and refreshes it through a topology source.
Currently the topology source is Picodata's system tables/functions.
Refreshes happen:
- once, synchronously, inside start() (before the timer loop task is created)
- on a timer, every
intervalseconds -- only ifintervalis notNoneandstart()was called. - on demand: await refresh_now() refreshes immediately, regardless of the timer.
- via the sync trigger(): doesn't refresh by itself, it just wakes the timer loop so it calls refresh_now() right away instead of waiting out the rest of the interval (e.g. the pool does this when a query fails with a connection error). Does nothing if the timer loop isn't running.
All ways funnel through the same lock, so only one refresh runs at a time.
Source code in picopyn/asynchronous/topology_tracker.py
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 | |
refresh_now()
async
Refresh topology immediately, regardless of the timer.
Source code in picopyn/asynchronous/topology_tracker.py
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 | |
start(on_refresh=None)
async
Start the tracker.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
on_refresh
|
Callable[[Topology], Awaitable[None]] | None
|
Awaitable callback, called with |
None
|
Source code in picopyn/asynchronous/topology_tracker.py
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | |
stop()
async
Stop the tracker.
Source code in picopyn/asynchronous/topology_tracker.py
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 | |
terminate()
Stop the tracker without waiting for graceful source close.
Source code in picopyn/asynchronous/topology_tracker.py
306 307 308 309 310 311 312 313 314 315 | |
trigger()
Wake the timer loop so it refreshes right now, instead of waiting.
It should be called if some SQL handler (fetch, execute etc.) fails with a closed connection.
Does nothing if the loop isn't running (interval=None, or start() was
never called).
Source code in picopyn/asynchronous/topology_tracker.py
372 373 374 375 376 377 378 379 380 381 | |
update_interval(interval)
Change the refresh interval and wake the background loop.
A refresh already in progress is allowed to finish.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
interval
|
float
|
Number of seconds between topology refreshes. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If periodic topology tracking is disabled. |
Source code in picopyn/asynchronous/topology_tracker.py
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 | |
picopyn.asynchronous.metadata_service
QueryMetadataService
Dedicated connection for discovering Picodata's query metadata.
Instead of every connection in the pool requesting metadata and passively filling a shared cache, one dedicated connection handles discovery -- separate from the connections that run real queries. Metadata depends only on the query text and the current schema, not on which connection asked/node answered, so a single connection and cache are enough, and ordinary queries never pay for this extra Notice traffic.
So, service owns the single connection and cache used to resolve query meta for
a Pool.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connect
|
Callable[[Callable[[PreparedStatementMetadata], None]], Awaitable[Connection | None]]
|
Opens a new dedicated
|
required |
cache_size
|
int
|
Maximum number of distinct queries to keep meta for. Oldest entries are evicted once exceeded. Must be at least 1. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
invalid |
Source code in picopyn/asynchronous/metadata_service.py
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 | |
close()
async
Waits for any background prepares still running, then closes the dedicated connection, if one is open.
Clears the cache. The stopped service no longer accepts metadata requests.
Source code in picopyn/asynchronous/metadata_service.py
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 | |
evict(query)
Drop a single cached entry, e.g. after a stale-plan error (see
is_stmt_invalidated_error).
Also lets background warming ask about the query again, if it will be requested by Pool etc
Source code in picopyn/asynchronous/metadata_service.py
219 220 221 222 223 224 225 | |
get_query_metadata(query)
async
Return Picodata's distribution key metadata for query, waiting
for it if needed.
Returns the cached metadata right away if present. On a cache miss,
it asks Picodata for the metadata and waits for the answer -- the
result is None if Picodata never sends metadata for this query
(e.g. it couldn't resolve a single bucket_id for it), or if no node
was reachable to ask.
Source code in picopyn/asynchronous/metadata_service.py
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | |
request_if_missing(query)
Return cached metadata for query, warming it in the background if missing.
Unlike
get_query_metadata,
this returns immediately. On a cache hit it returns the cached metadata.
On a cache miss it schedules metadata discovery and returns None.
Pool.execute calls this
(only for parameterized queries; meta is only ever populated
for INSERT, see
PreparedStatementMetadata)
so a query with no cached metadata yet still runs right away on an
ordinary pool connection, while Picodata is asked for the metadata
in the background on the dedicated connection.
---
title: Background job -- get and cache query meta
---
flowchart TD
classDef neutral fill:#f5f5f5,stroke:#9e9e9e,color:#424242
classDef note fill:#fffde7,stroke:#f9a825,stroke-dasharray:4,color:#555
QUERY(["parametrized query"]):::neutral
QUERY --> PARSE_QUERY
PARSE_QUERY["ask Picodata to parse query"] -. Parse .-> PICO[("Picodata")] -. Notice message .-> GET
GET["receive the notice"] --> PARSE_MSG{"is this query metadata?"}
NOTE_ASYNC>"the callback must run<br>synchronously -- metadata has<br>to be ready before the caller's<br>request completes"]:::note -.-> GET
CACHE[/"Pool query meta cache"/] -. write .- FORWARD
PARSE_MSG --> |parsed| FORWARD(["store parsed meta<br>in the cache"]):::neutral
PARSE_MSG --> |could not parse picodata<br>notice message, e.g. Picodata<br>couldn't resolve a distribution<br>key for this query| EXIT(["stop silently"]):::neutral
Source code in picopyn/asynchronous/metadata_service.py
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | |
resize(cache_size)
Resize the cache and the limit for pending background prepares.
Already pending prepares are allowed to finish. New ones are admitted only when their count is below the new limit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cache_size
|
int
|
Maximum number of cached and pending queries. |
required |
Source code in picopyn/asynchronous/metadata_service.py
72 73 74 75 76 77 78 79 80 81 82 | |
start()
async
Eagerly opens the dedicated connection.
Source code in picopyn/asynchronous/metadata_service.py
84 85 86 87 88 | |
stop()
async
Stop the service, cancel pending work and close its dedicated connection.
Source code in picopyn/asynchronous/metadata_service.py
243 244 245 246 | |
terminate()
Terminate the dedicated connection without waiting for graceful close.
Source code in picopyn/asynchronous/metadata_service.py
248 249 250 251 252 253 254 255 256 | |