Sync: 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.synchronous.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.synchronous.topology_tracker.TopologyTracker" "Topology tracker details"
SOURCE_DETAILS_NOTE>"See topology source details"]:::note -.-> SOURCE
click SOURCE_DETAILS_NOTE "#picopyn.synchronous.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/synchronous/topology_tracker.py
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 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 | |
refresh(topology)
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/synchronous/topology_tracker.py
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 | |
TopologySource
Bases: ABC
Something that can bring a Topology up to date.
Source code in picopyn/synchronous/topology_tracker.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | |
cancel()
Interrupt a refresh() running in another thread, if the source can.
Unlike close() it must be safe to call while a refresh is in flight.
Sources that cannot be interrupted just let the refresh finish, so the
default is a no-op.
Source code in picopyn/synchronous/topology_tracker.py
75 76 77 78 79 80 81 | |
close()
abstractmethod
Release any resources (e.g. connections) this source holds open.
Only safe to call when no refresh() is running: it may drop resources
that a running refresh is using. To interrupt a refresh running in another
thread use cancel() instead.
Source code in picopyn/synchronous/topology_tracker.py
65 66 67 68 69 70 71 72 73 | |
TopologyTracker
Holds a topology view and refreshes it through a topology source.
Currently the topology source is Picodata's system tables/functions.
Refreshes happen:
- on a timer, every
intervalseconds -- only ifintervalis notNoneandstart()was called. - on demand:
refresh_now()refreshes immediately, regardless of the timer. - via
trigger(): it doesn't refresh by itself, it just wakes the timer loop so it callsrefresh_now()right away instead of waiting out the rest of the interval. Does nothing if the timer loop isn't running.
All ways share the same lock, so only one refresh runs at a time.
Source code in picopyn/synchronous/topology_tracker.py
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 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 | |
find_bucket_master_uuid(tier, bucket_id)
Return the current master instance UUID for a bucket in a tier.
Source code in picopyn/synchronous/topology_tracker.py
446 447 448 449 450 | |
get_bucket_count(tier)
Return the known bucket count for a tier.
Source code in picopyn/synchronous/topology_tracker.py
436 437 438 439 | |
has_tiers()
Return whether any tier bucket counts are known.
Source code in picopyn/synchronous/topology_tracker.py
441 442 443 444 | |
refresh_now()
Refresh topology immediately, regardless of the timer.
Source code in picopyn/synchronous/topology_tracker.py
452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 | |
snapshot()
Return a consistent detached copy of the current topology view.
Source code in picopyn/synchronous/topology_tracker.py
431 432 433 434 | |
start(on_refresh=None)
Start the tracker.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
on_refresh
|
Callable[[Topology], None] | None
|
Callback, called with |
None
|
Source code in picopyn/synchronous/topology_tracker.py
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 | |
stop(timeout=5.0)
Stop the topology update loop.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout
|
float | None
|
Maximum number of seconds to wait for the update loop to stop. None waits until the loop has finished and closed its source. |
5.0
|
Source code in picopyn/synchronous/topology_tracker.py
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 | |
terminate()
Stop the update loop without waiting for it to finish.
Unlike stop() it never joins the loop thread. A refresh stuck on a query is cancelled instead, so it fails right away and the loop winds down on its own, closing the source from the thread that owns it. Closing the source here would mean closing a connection another thread is running a query on, which the driver doesn't allow.
Source code in picopyn/synchronous/topology_tracker.py
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 | |
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/synchronous/topology_tracker.py
491 492 493 494 495 496 497 498 499 500 | |
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/synchronous/topology_tracker.py
470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 | |
picopyn.synchronous.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]], 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/synchronous/metadata_service.py
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 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 | |
close()
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/synchronous/metadata_service.py
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 | |
evict(query)
Drop a single cached entry, e.g. after a stale-plan error (see
is_stmt_invalidated_error).
Source code in picopyn/synchronous/metadata_service.py
263 264 265 266 267 268 | |
get_query_metadata(query)
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/synchronous/metadata_service.py
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | |
request_if_missing(query, parameters=None)
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
Only one background fetch per query can be queued at a time. If too many distinct queries are already pending, warming is skipped: the caller still executes the query normally, just without cache warming. A query Picodata already answered nothing about is not asked about again either, until the cached view goes stale.
Source code in picopyn/synchronous/metadata_service.py
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 | |
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/synchronous/metadata_service.py
78 79 80 81 82 83 84 85 86 87 88 89 90 | |
start()
Eagerly opens the dedicated connection.
Source code in picopyn/synchronous/metadata_service.py
92 93 94 95 96 | |
stop()
Stop the service, cancel pending work and close its dedicated connection.
Source code in picopyn/synchronous/metadata_service.py
286 287 288 289 | |
terminate()
Close the dedicated connection without waiting for background prepares.
Unlike close()
it never joins the worker thread. A prepare stuck on a query is cancelled
instead, so it fails right away and _prepare closes the connection from the
thread that owns it. Closing it here would mean closing a connection another
thread is running a query on, which the driver doesn't allow.
Source code in picopyn/synchronous/metadata_service.py
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 | |