Topology
picopyn.topology
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 topology tracking actually works.
Client-side view of the Picodata cluster topology.
Based on the topology notifications ADR.
Picodata doesn't push topology changes yet as server-side events. So for now we get this data by periodically polling Picodata's system tables and scalar functions with the Topology Tracker instead.
Note
That function itself only exists starting with a 25.5.1 Picodata version, so on older clusters bucket tracking simply stays empty while instances/replicasets keep working.
Once Picodata starts pushing real events, we'll only need to change the source of topology (from Picodata's system tables/functions to NOTIFY messages).
See details in Topology.
Note
Topology itself is read-only -- it only lets callers look at the current cluster state.
When topology tracking is enabled, the pool reacts to topology changes in both modes and
adjusts its connections to match (see async reconcile and
sync reconcile).
Bucket
dataclass
A range of buckets owned by a replicaset.
Attributes:
| Name | Type | Description |
|---|---|---|
bucket_id_start |
int
|
First bucket id in the range (inclusive). Also the
second half of this entry's key in |
bucket_id_end |
int
|
Last bucket id in the range (inclusive). |
current_replicaset_uuid |
str
|
Uuid of the replicaset that currently owns this range. |
Source code in picopyn/topology.py
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | |
BucketRange
dataclass
An inclusive range of bucket ids, e.g. [start, end].
Attributes:
| Name | Type | Description |
|---|---|---|
start |
int
|
First bucket id in the range (inclusive). |
end |
int
|
Last bucket id in the range (inclusive). |
Source code in picopyn/topology.py
102 103 104 105 106 107 108 109 110 111 112 | |
EventBuckets
dataclass
A bucket range change: reassigned to a different replicaset, etc.
Attributes:
| Name | Type | Description |
|---|---|---|
op |
Op
|
Carried for structural parity with the ADR event, but ignored --
there can be only |
tier |
str
|
Tier this bucket range belongs to. |
state |
str
|
vshard-reported state of the range, e.g. |
bucket_id |
BucketRange
|
Range of bucket ids being (re)assigned. |
current_replicaset_uuid |
str
|
Uuid of the replicaset that now owns this range. |
Source code in picopyn/topology.py
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | |
EventInstance
dataclass
An instance change: status changed, instance added, role changed, etc.
A few fields are missing here -- Picodata doesn't send this event yet, so there's nothing real to put in them.
Attributes:
| Name | Type | Description |
|---|---|---|
op |
Op
|
|
instance_uuid |
str
|
Which instance this event is about. |
replicaset_uuid |
str | None
|
New replicaset uuid, or |
current_state |
str | None
|
New state, or |
address |
str | None
|
New |
Source code in picopyn/topology.py
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | |
EventReplicaset
dataclass
A replicaset change: replicaset added, master changed, etc.
A few fields are missing here -- Picodata doesn't send this event yet, so there's nothing real to put in them.
Attributes:
| Name | Type | Description |
|---|---|---|
op |
Op
|
|
replicaset_uuid |
str
|
Which replicaset this event is about. |
tier |
str | None
|
New tier, or |
current_master_uuid |
str | None
|
New replication leader uuid, or |
Source code in picopyn/topology.py
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | |
Instance
dataclass
A Picodata instance, assembled from _pico_instance and
_pico_peer_address rows.
Not tier-tagged -- look up replicasets[replicaset_uuid].tier instead.
Attributes:
| Name | Type | Description |
|---|---|---|
uuid |
str
|
Instance uuid. |
replicaset_uuid |
str | None
|
Uuid of the replicaset this instance belongs to,
or |
current_state |
str | None
|
e.g. |
address |
str | None
|
|
is_online |
bool
|
Whether current instance state is online (read-only property). |
Source code in picopyn/topology.py
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 | |
is_online
property
Whether the instance is in the online state.
Replicaset
dataclass
A Picodata replicaset, assembled from _pico_replicaset rows.
Attributes:
| Name | Type | Description |
|---|---|---|
uuid |
str
|
Replicaset uuid. |
tier |
str | None
|
Tier this replicaset belongs to, or |
current_master_uuid |
str | None
|
Uuid of the current replication leader instance,
or |
Source code in picopyn/topology.py
162 163 164 165 166 167 168 169 170 171 172 173 174 175 | |
Topology
Holds the current view of the cluster topology known to the client.
Attributes:
| Name | Type | Description |
|---|---|---|
instances |
dict[str, Instance]
|
Known instances, keyed by instance uuid. |
replicasets |
dict[str, Replicaset]
|
Known replicasets, keyed by replicaset uuid. |
buckets |
dict[tuple[str, int], Bucket]
|
Known bucket ranges, keyed by |
tiers |
dict[str, int]
|
Total bucket count per tier ( |
The topology tracker updates the topology on a schedule or when triggered:
---
title: Topology tracking
---
flowchart TD
classDef neutral fill:#f5f5f5,stroke:#9e9e9e,color:#424242
classDef note fill:#fffde7,stroke:#f9a825,stroke-dasharray:4,color:#555
ADR_DETAILS_NOTE>"Based on the Picodata topology<br>notifications ADR"]:::note
click ADR_DETAILS_NOTE "https://git.picodata.io/core/picodata/-/blob/master/doc/adr/2025-09-12-pgproto-topology-notifications.md" "Picodata ADR"
TOPOLOGY[[Topology<br>·instances<br>·replicasets<br>·buckets<br>·tiers]]
CONNECT["Pool.open()"] --> CFG[/"Topology tracker settings<br>· update interval"/] --> START["start topology tracker"] --> INITIAL["initial refresh<br>(sync, imitate ReadyForQuery)"] --> WAIT
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
INITAL_DETAILS_NOTE -.-> INITIAL
subgraph RefreshLoop["Topology tracker loop"]
direction TB
WAIT{"timer elapsed<br>or triggered?"} --> REFRESH["refresh topology"]
end
REFRESH -.->|next iteration| WAIT
REFRESH -.-> TOPOLOGY
QUERY["Pool.execute / fetch / fetchrow"] --> ERR{"connection error?"}
ERR -->|yes| TRIGGER["call tracker trigger"]
ERR -->|no| RAISE(["do not trigger tracker"])
TRIGGER -.-> WAIT
READ["pool.topology"] -.-> TOPOLOGY
NOTE>"read-only view<br>(but can be used to<br>change pool membership)"]:::note
NOTE -.-> READ
Currently, the refresh polls Picodata's system tables/functions:
---
title: Topology refresh
---
flowchart TD
classDef neutral fill:#f5f5f5,stroke:#9e9e9e,color:#424242
classDef note fill:#fffde7,stroke:#f9a825,stroke-dasharray:4,color:#555
START(("START")) --> FETCH
TOPOLOGY[/Topology:<br>- instances<br>- replicasets<br>- buckets<br>- tiers/]
FETCH["fetch Picodata system tables"] --> PARSE["parse db rows"]
FETCH <-. read .-> SYS_TABLES[(Picodata system tables)]
PARSE --> DIFF["compare current topology<br>keys with rows"]
TOPOLOGY1[/Topology/] <-. read .-> DIFF
DIFF -.-> EVENTS[["- missing entries become delete events<br>- the rest become replace events<br>"]]
EVENTS -.-> APPLY["topology apply<br>instance/replicaset events"]
TOPOLOGY2[/Topology/] <-. write .-> APPLY
APPLY --> TIERS_FETCHED{"tiers already<br>fetched for this<br>connection?"}
TIERS_FETCHED -->|no| TIER_FETCH["fetch tier names<br>+ bucket counts"] --> TIER_APPLY["topology apply<br>tiers"]
TIER_FETCH <-. read .-> TIER_TABLE[("_pico_tier")]
TIER_APPLY <-. write .-> TOPOLOGY_TIERS[/Topology/]
TIERS_FETCHED -->|yes| COLLECT_TIER
TIER_APPLY --> COLLECT_TIER[["collect tier list<br>from topology"]]
TIER_NOTE>"_pico_tier rows are written once<br>at cluster formation and never<br>rewritten -- fetched at most once<br>per connection, then trusted"]:::note
TIER_NOTE -.-> TIERS_FETCHED
COLLECT_TIER <-. read .-> TOPOLOGY3[/Topology/]
COLLECT_TIER --> PerTier
subgraph PerTier["for each known tier"]
direction TB
BUCKET_FETCH["fetch tier's bucket info"] --> BUCKET_PARSE["parse db rows"]
BUCKET_FETCH <-. read .-> BUCKET_FN[("_pico_bucket(tier)"<br>function)]
BUCKET_PARSE -.-> BUCKET_EVENTS[["generated bucket events"]]
BUCKET_EVENTS -.-> BUCKET_APPLY["topology apply<br>bucket events"]
end
BUCKETS_DETAILS_NOTE>"Bucket event details"]:::note
click BUCKETS_DETAILS_NOTE "#picopyn.topology.Topology.apply_bucket" "Bucket event details"
BUCKETS_DETAILS_NOTE -.-> BUCKET_APPLY
NOTE>"_pico_bucket(tier) may not exist<br>on older Picodata -- failure<br>is logged and skipped per tier,<br>not fatal"]:::note
NOTE -.-> BUCKET_FETCH
BUCKET_APPLY <-. write .-> TOPOLOGY4[/Topology/]
BUCKET_APPLY --> STOP(("STOP"))
Source code in picopyn/topology.py
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 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 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 | |
apply_bucket(event)
Apply an EventBuckets message by updating the bucket range it describes.
There are several cases that can trigger bucket migration, for example:
- a replicaset is added to the cluster
- a replicaset is removed from the cluster
- a replicaset's weight changes
Note
Picodata doesn't specify how it will notify about bucket migrations or replicaset membership changes -- the topology notifications ADR only covers the initial topology snapshot. We accept the risk that this algorithm may need to change once real behavior is observed. For now we assume:
- Events keep arriving replicaset -> instance -> bucket (the ADR's initial-snapshot order) even for later changes, so a replicaset's uuid/tier is always known by the time its bucket events arrive.
- A distribution change resends a bucket event for every replicaset affected in that tier -- not for unaffected replicasets, and not for replicasets that no longer exist.
Therefore, replicaset events can also affect the bucket map:
---
title: Replicaset Event Processing (Bucket Context)
---
flowchart TD
classDef neutral fill:#f5f5f5,stroke:#9e9e9e,color:#424242
classDef note fill:#fffde7,stroke:#f9a825,stroke-dasharray:4,color:#555
classDef spacer fill:none,stroke:none
NOTE>"If a tier's bucket map becomes<br>empty after processing a replicaset<br>event, it will be populated later<br>by bucket events"]:::note
NOTE2>"Topology consists of:<br>- replicaset list<br>- instances list<br>- bucket maps<br>Bucket maps are indexed by tier name<br>and store bucket ranges<br>and replicaset UUIDs."]:::note
EVENT[/"Replicaset Event<br>- event type<br>- replicaset uuid<br>- current master uuid"/]:::neutral --> APPLY["apply replicaset event<br>to topology"]
TOPOLOGY_REPL[/"Topology.replicasets"/] -. update .- APPLY
APPLY --> EVENT_TYPE{"event type?"}
EVENT_TYPE --> DelEvent
EVENT_TYPE --> NewEvent
EVENT_TYPE --> ChangeEvent
subgraph DelEvent["delete replicaset"]
direction TB
CLEAR_DEL["clear tier bucket map"]
CLEAR_DEL -. update .- TOPOLOGY_DEL_BUCK[/"Topology.buckets[tier]"/]
end
subgraph NewEvent["new replicaset"]
direction TB
TIER_CHECK{"is tier already registered?"}
TOPOLOGY_NEW_BUCK1[/"Topology.buckets"/]
TIER_CHECK -. read .- TOPOLOGY_NEW_BUCK1
TIER_CHECK --> |yes| CLEAR_NEW["clear tier bucket map"]
TIER_CHECK --> |no| TIER_ADD["create an empty<br>tier bucket map"]
TOPOLOGY_NEW_BUCK2[/"Topology.buckets[tier]"/]
TIER_ADD -. update .- TOPOLOGY_NEW_BUCK2
CLEAR_NEW -. update .- TOPOLOGY_NEW_BUCK2
end
subgraph ChangeEvent["existing replicaset changed<br>(e.g. master switched)"]
direction TB
S1[" "]:::spacer --> NOOP["nothing to do"] --> S2[" "]:::spacer
end
DelEvent --> DONE(["replicaset event<br>processed"]):::neutral
NewEvent --> DONE2(["replicaset event<br>processed"]):::neutral
ChangeEvent --> DONE3(["replicaset event<br>processed"]):::neutral
---
title: Bucket Event Processing
---
flowchart TD
classDef neutral fill:#f5f5f5,stroke:#9e9e9e,color:#424242
EVENT[/"Bucket Event<br>- tier T<br>- bucket start A<br>- bucket end B<br>- replicaset uuid X"/]:::neutral
TOPOLOGY_BUCK[/"Topology.buckets[tier=T]<br>- bucket range 1 -> uuid1<br>- ...<br>- bucket range N -> uuidN"/] -. read .- CHECK
EVENT --> CHECK{"Do the received<br>buckets [A, B] overlap<br>existing bucket ranges in tier T?"}
CHECK -->|yes| CLEAR["clear tier T bucket map<br>(the map can no longer be trusted)"]
CLEAR -. update .- TOPOLOGY_EMPTY_BUCK[/"Topology.buckets[tier=T]<br>(empty)"/] --> APPLY
CHECK -->|no| APPLY["upsert the received<br>(bucket range -> replicaset uuid)<br>into tier T bucket map"]
APPLY -. upsert .- TOPOLOGY_NEW_BUCK[/"Topology.buckets[tier=T]<br>- range [A, B] -> uuid X<br>- ...(unchanged mappings)"/]
TOPOLOGY_NEW_BUCK --> DONE(["bucket event<br>processed"]):::neutral
Source code in picopyn/topology.py
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 | |
apply_instance(event)
Apply an instance event.
Source code in picopyn/topology.py
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 | |
apply_replicaset(event)
Apply a replicaset event.
A replicaset joining or leaving a tier changes how that tier's buckets are distributed, so the replicaset event affects bucket map.
Source code in picopyn/topology.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 316 317 318 319 320 321 322 323 | |
apply_tiers(tiers)
Apply a tier name -> bucket_count snapshot read from the system table.
Unlike applying instance/replicaset/bucket events, there's no replace/delete event semantics for tiers. We need it only because Picodata does not send topology Notifications right now. Tiers are static cluster configuration, fixed at cluster formation, so once applied, later calls are ignored.
Note
Currently read from system table. Once Picodata starts sending Notifications, tier bucket counts can simply be calculated as the sum of each tier's replicaset buckets once Picodata sends ReadyForQuery message telling us our Topology view is now consistent.
(see Picodata's topology notifications ADR)
Source code in picopyn/topology.py
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | |
clear()
Forget everything we know about the topology.
Do this before reading a fresh snapshot from a new connection -- the new connection may be talking to an instance that's behind or ahead of the one we had before.
Note
Does not clear tiers -- it's static cluster configuration, not
tied to which instance we're connected to.
Source code in picopyn/topology.py
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 | |
find_bucket_master(tier, bucket_id)
Find the current master instance for a bucket in a tier.
Returns None if the topology snapshot is incomplete: the bucket range
is unknown, its replicaset is unknown, the replicaset has no known
master yet, or the master instance itself has not been observed.
Source code in picopyn/topology.py
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 | |
known_tiers()
Names of all tiers in the cluster.
Empty until tiers has been populated -- and once it is, the
result never changes, since tiers are fixed for the cluster's
lifetime.
Returns:
| Type | Description |
|---|---|
set[str]
|
The set of tier names, or an empty set if |
set[str]
|
been populated yet. |
Source code in picopyn/topology.py
469 470 471 472 473 474 475 476 477 478 479 480 | |
online_addresses()
Addresses of instances currently in the Online state.
Source code in picopyn/topology.py
433 434 435 436 437 438 439 | |
snapshot()
Return a detached copy of the current topology view.
Source code in picopyn/topology.py
228 229 230 | |