| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * Copyright (c) 2026 Tiger Data, Inc. | ||
| 3 | * Licensed under the PostgreSQL License. See LICENSE for details. | ||
| 4 | * | ||
| 5 | * maintenance.c - mutating index maintenance functions | ||
| 6 | * | ||
| 7 | * SQL-callable operations that modify prism index pages, and therefore | ||
| 8 | * WAL-log them. Kept separate from the strictly read-only inspection | ||
| 9 | * functions in inspect.c so the write/WAL paths are grouped where they | ||
| 10 | * get the scrutiny persistent mutations need. | ||
| 11 | * | ||
| 12 | * Operations: | ||
| 13 | * prism_convert_posting_to_fastscan(regclass, int4) -- convert one cluster's | ||
| 14 | * posting chain from AoS to fastscan format (procedure) | ||
| 15 | * prism_split_posting_list(regclass, bigint) -- split one list by head block | ||
| 16 | * (procedure) | ||
| 17 | * prism_rebalance(regclass, int4) -- split every list over a given size | ||
| 18 | * (procedure) | ||
| 19 | * | ||
| 20 | * All three are procedures, not functions: they are DDL-like mutating | ||
| 21 | * maintenance and take no return value (reporting via NOTICE). Only | ||
| 22 | * split_posting_list and rebalance additionally require their own | ||
| 23 | * transaction (require_own_transaction): they manage a maintenance pass | ||
| 24 | * across possibly many lists, and letting them run inside a caller's | ||
| 25 | * transaction would offer a rollback that does not roll back the pages | ||
| 26 | * they have already written. convert_posting_to_fastscan performs one | ||
| 27 | * atomic write with no such internal boundary, so it carries no such | ||
| 28 | * restriction -- it is meant to be called in a loop from a DO block or | ||
| 29 | * function, e.g. to convert every cluster a query selects. | ||
| 30 | */ | ||
| 31 | |||
| 32 | #include <postgres.h> | ||
| 33 | |||
| 34 | #include <access/generic_xlog.h> | ||
| 35 | #include <access/htup_details.h> | ||
| 36 | #include <access/relation.h> | ||
| 37 | #include <access/reloptions.h> | ||
| 38 | #include <access/table.h> | ||
| 39 | #include <access/tableam.h> | ||
| 40 | #include <access/transam.h> | ||
| 41 | #include <access/xact.h> | ||
| 42 | #include <catalog/index.h> | ||
| 43 | #include <catalog/indexing.h> | ||
| 44 | #include <catalog/objectaddress.h> | ||
| 45 | #include <catalog/pg_class.h> | ||
| 46 | #include <executor/tuptable.h> | ||
| 47 | #include <fmgr.h> | ||
| 48 | #include <miscadmin.h> | ||
| 49 | #include <nodes/makefuncs.h> | ||
| 50 | #include <nodes/parsenodes.h> | ||
| 51 | #include <storage/bufmgr.h> | ||
| 52 | #include <storage/lmgr.h> | ||
| 53 | #include <utils/acl.h> | ||
| 54 | #include <utils/builtins.h> | ||
| 55 | #include <utils/injection_point.h> | ||
| 56 | #include <utils/inval.h> | ||
| 57 | #include <utils/lsyscache.h> | ||
| 58 | #include <utils/rel.h> | ||
| 59 | #include <utils/snapmgr.h> | ||
| 60 | #include <utils/syscache.h> | ||
| 61 | |||
| 62 | #include "amcache.h" | ||
| 63 | #include "build.h" | ||
| 64 | #include "index/centroid_page.h" | ||
| 65 | #include "index/index_base.h" | ||
| 66 | #include "index/index_build.h" | ||
| 67 | #include "index/posting_convert.h" | ||
| 68 | #include "index/posting_page.h" | ||
| 69 | #include "index/posting_split.h" | ||
| 70 | #include "inspect.h" | ||
| 71 | #include "meta.h" | ||
| 72 | #include "pg/bufstorage.h" | ||
| 73 | #include "support_pg.h" | ||
| 74 | #include "typeinfo.h" | ||
| 75 | #include "types/vec32.h" | ||
| 76 | |||
| 77 | /* | ||
| 78 | * Lock mode the mutating maintenance entry points take on the index. | ||
| 79 | * | ||
| 80 | * ShareUpdateExclusiveLock rather than RowExclusiveLock, because it conflicts | ||
| 81 | * with itself: two maintenance calls on one index have to serialize. A split | ||
| 82 | * mints ids for its new clusters from the leaf count it read (nlist + j) and | ||
| 83 | * writes the updated count back when it finishes, so two overlapping passes | ||
| 84 | * would hand the same ids to different lists and persist a count too low by | ||
| 85 | * one pass's worth -- which then undercounts leaves for the automatic probe | ||
| 86 | * count and the cost model, and leaves cluster ids ambiguous for | ||
| 87 | * convert_posting_to_fastscan and tids_clusters. | ||
| 88 | * | ||
| 89 | * Like VACUUM's lock (the same mode, for the same "one housekeeper at a time" | ||
| 90 | * reason) it still admits reads and inserts, so maintenance does not block | ||
| 91 | * queries. | ||
| 92 | * | ||
| 93 | * Held until end of transaction, not until the relation is closed: the new | ||
| 94 | * leaf count reaches other backends as a relcache invalidation, which is only | ||
| 95 | * delivered at commit. | ||
| 96 | */ | ||
| 97 | #define PRISM_MAINT_LOCK ShareUpdateExclusiveLock | ||
| 98 | |||
| 99 | /* | ||
| 100 | * Refuse to run inside a caller's transaction. | ||
| 101 | * | ||
| 102 | * These procedures reorganize the index; they do not change the data it points | ||
| 103 | * at. Their page writes are not transactional either -- a ROLLBACK leaves the | ||
| 104 | * lists split, the old chains retired and the leaf count raised, while | ||
| 105 | * discarding the relcache invalidation that tells other backends the leaf | ||
| 106 | * count moved. Letting them run inside a transaction block therefore offers a | ||
| 107 | * rollback that does not roll anything back. | ||
| 108 | * | ||
| 109 | * It is also what lets a pass commit as it goes. Splitting each list in its | ||
| 110 | * own transaction -- so a long pass bounds its transaction and can reclaim | ||
| 111 | * what its earlier splits retired -- is impossible from inside a caller's | ||
| 112 | * transaction, which is the reason a procedure was the right shape for this to | ||
| 113 | * begin with. | ||
| 114 | * | ||
| 115 | * The same test VACUUM makes, in the form a procedure has available: a | ||
| 116 | * top-level CALL gets a non-atomic call context, anything else (a transaction | ||
| 117 | * block, a function, a DO block) gets an atomic one. | ||
| 118 | */ | ||
| 119 | static void | ||
| 120 | 51 | require_own_transaction(FunctionCallInfo fcinfo, const char *procname) | |
| 121 | { | ||
| 122 | 155 | CallContext *cc = (fcinfo->context != NULL && | |
| 123 |
1/2✓ Branch 0 taken 51 times.
✗ Branch 1 not taken.
|
51 | IsA(fcinfo->context, CallContext)) |
| 124 | ? (CallContext *)fcinfo->context | ||
| 125 |
1/2✓ Branch 0 taken 51 times.
✗ Branch 1 not taken.
|
51 | : NULL; |
| 126 | |||
| 127 |
2/2✓ Branch 0 taken 49 times.
✓ Branch 1 taken 2 times.
|
51 | if (cc != NULL && !cc->atomic) |
| 128 | 49 | return; | |
| 129 | |||
| 130 |
1/2✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
|
51 | ereport(ERROR, |
| 131 | (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION), | ||
| 132 | errmsg("%s cannot run inside a transaction block", procname), | ||
| 133 | errdetail( | ||
| 134 | "It reorganizes the index outside transaction control, " | ||
| 135 | "so a rollback would not undo it."), | ||
| 136 | errhint("Call it on its own, outside BEGIN/COMMIT."))); | ||
| 137 | } | ||
| 138 | |||
| 139 | /* | ||
| 140 | * A PROCEDURE cannot be declared STRICT, so a NULL argument arrives here as a | ||
| 141 | * zero rather than short-circuiting to a NULL result. Left unchecked, a NULL | ||
| 142 | * index reads as OID 0 and fails with "could not open relation with OID 0", | ||
| 143 | * and a NULL block number as block 0 -- errors that describe the internal | ||
| 144 | * consequence instead of the caller's mistake. | ||
| 145 | */ | ||
| 146 | static void | ||
| 147 | 705 | reject_null_arg(FunctionCallInfo fcinfo, int argno, const char *argname) | |
| 148 | { | ||
| 149 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 702 times.
|
705 | if (PG_ARGISNULL(argno)) |
| 150 |
1/2✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
|
3 | ereport(ERROR, |
| 151 | (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), | ||
| 152 | errmsg("%s must not be null", argname))); | ||
| 153 | 702 | } | |
| 154 | |||
| 155 | 10 | PG_FUNCTION_INFO_V1(vs_convert_posting_to_fastscan); | |
| 156 | 11 | PG_FUNCTION_INFO_V1(vs_split_posting_list); | |
| 157 | 18 | PG_FUNCTION_INFO_V1(vs_rebalance); | |
| 158 | |||
| 159 | /* | ||
| 160 | * Authorization. convert_posting_to_fastscan mutates the index, so it requires | ||
| 161 | * ownership of the *table* the index belongs to -- the same relation model | ||
| 162 | * PostgreSQL's pgrowlocks/pgstattuple use for relation-level operations. | ||
| 163 | * EXECUTE stays granted to PUBLIC; this runtime check does the per-object | ||
| 164 | * authorization a static GRANT cannot express for a regclass argument. | ||
| 165 | * Superusers pass automatically. | ||
| 166 | */ | ||
| 167 | static void | ||
| 168 | 369 | require_index_owner(Relation index, LOCKMODE lockmode) | |
| 169 | { | ||
| 170 | 369 | Oid heaprelid = IndexGetRelation(RelationGetRelid(index), false); | |
| 171 |
2/2✓ Branch 2 taken 4 times.
✓ Branch 3 taken 365 times.
|
369 | if (!object_ownercheck(RelationRelationId, heaprelid, GetUserId())) |
| 172 | { | ||
| 173 | 4 | char *relname = get_rel_name(heaprelid); | |
| 174 | 4 | ObjectType objtype = get_relkind_objtype(get_rel_relkind(heaprelid)); | |
| 175 | 4 | relation_close(index, lockmode); | |
| 176 | 4 | aclcheck_error(ACLCHECK_NOT_OWNER, objtype, relname); | |
| 177 | } | ||
| 178 | 365 | } | |
| 179 | |||
| 180 | /* | ||
| 181 | * Writable pointer to a leaf entry's child block number on a centroid page. | ||
| 182 | * The location depends on the page format: an AoS meta array for the | ||
| 183 | * meta-based formats, or the packed per-group child array for a fastscan | ||
| 184 | * centroid page. This must match how collect_leaf_entries reads the child, or | ||
| 185 | * the compare-and-set in update_centroid_posting_head acts on the wrong slot. | ||
| 186 | */ | ||
| 187 | static BlockNumber * | ||
| 188 | 638 | centroid_child_ptr(Page page, uint16_t entry_idx, Dimension dim) | |
| 189 | { | ||
| 190 |
2/2✓ Branch 1 taken 574 times.
✓ Branch 2 taken 64 times.
|
638 | if (prism_centroid_page_format(page) == PRISM_CENTROID_FMT_FASTSCAN) |
| 191 | { | ||
| 192 | 574 | char *content = (char *)PageGetContents(page); | |
| 193 | 574 | uint32_t g = entry_idx / VS_FASTSCAN_GROUP; | |
| 194 | 574 | uint32_t slot = entry_idx % VS_FASTSCAN_GROUP; | |
| 195 | 574 | return &prism_centroid_fastscan_group_child(content, g, dim)[slot]; | |
| 196 | } | ||
| 197 | 64 | return &prism_centroid_meta_mut(page, entry_idx)->child_blkno; | |
| 198 | } | ||
| 199 | |||
| 200 | /* | ||
| 201 | * Point a centroid leaf entry at new_head via WAL, but only if it still | ||
| 202 | * points at expected_old_head. The compare-and-set runs under the same | ||
| 203 | * exclusive lock as the write, with no gap. The maintenance entry points hold | ||
| 204 | * a self-conflicting lock on the index (PRISM_MAINT_LOCK), so two | ||
| 205 | * of them cannot race on one cluster in the first place -- but the | ||
| 206 | * compare-and-set stays as a gate that does not depend on callers agreeing | ||
| 207 | * about lock modes. The head pointer -- not the posting page's fastscan flag | ||
| 208 | * -- is what conversion actually updates, so it is the correct thing to test. | ||
| 209 | * | ||
| 210 | * Returns true if it performed the update. Returns false if another | ||
| 211 | * converter already moved the head, writing the current head to | ||
| 212 | * *current_head_out (the caller's freshly built chain is then orphaned, | ||
| 213 | * to be reclaimed by a later rebuild/VACUUM). | ||
| 214 | */ | ||
| 215 | static bool | ||
| 216 | 319 | update_centroid_posting_head( | |
| 217 | Relation index, | ||
| 218 | BlockNumber centroid_page, | ||
| 219 | uint16_t entry_idx, | ||
| 220 | BlockNumber expected_old_head, | ||
| 221 | BlockNumber new_head, | ||
| 222 | Dimension dim, | ||
| 223 | BlockNumber *current_head_out) | ||
| 224 | { | ||
| 225 | 319 | Buffer buf = ReadBuffer(index, centroid_page); | |
| 226 | 319 | LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE); | |
| 227 | |||
| 228 |
1/2✗ Branch 2 not taken.
✓ Branch 3 taken 319 times.
|
319 | if (*centroid_child_ptr(BufferGetPage(buf), entry_idx, dim) != |
| 229 | expected_old_head) | ||
| 230 | { | ||
| 231 | ✗ | *current_head_out = | |
| 232 | ✗ | *centroid_child_ptr(BufferGetPage(buf), entry_idx, dim); | |
| 233 | ✗ | UnlockReleaseBuffer(buf); | |
| 234 | ✗ | return false; | |
| 235 | } | ||
| 236 | |||
| 237 | 319 | GenericXLogState *state = GenericXLogStart(index); | |
| 238 | 319 | Page page = GenericXLogRegisterBuffer(state, buf, GENERIC_XLOG_FULL_IMAGE); | |
| 239 | 319 | *centroid_child_ptr(page, entry_idx, dim) = new_head; | |
| 240 | |||
| 241 | 319 | GenericXLogFinish(state); | |
| 242 | 319 | UnlockReleaseBuffer(buf); | |
| 243 | 319 | return true; | |
| 244 | } | ||
| 245 | |||
| 246 | /* | ||
| 247 | * Set PRISM_META_FLAG_FASTSCAN on the metadata page if not already set. | ||
| 248 | */ | ||
| 249 | static void | ||
| 250 | 319 | ensure_meta_fastscan_flag(Relation index) | |
| 251 | { | ||
| 252 | 319 | Buffer buf = ReadBuffer(index, 0); | |
| 253 | 319 | LockBuffer(buf, BUFFER_LOCK_SHARE); | |
| 254 | 319 | Page page = BufferGetPage(buf); | |
| 255 | 319 | PrismMetaPage *mp = (PrismMetaPage *)PageGetSpecialPointer(page); | |
| 256 | 319 | bool needs_update = !(mp->flags & PRISM_META_FLAG_FASTSCAN); | |
| 257 | 319 | UnlockReleaseBuffer(buf); | |
| 258 | |||
| 259 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 315 times.
|
319 | if (needs_update) |
| 260 | { | ||
| 261 | 4 | GenericXLogState *state = GenericXLogStart(index); | |
| 262 | 4 | buf = ReadBuffer(index, 0); | |
| 263 | 4 | LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE); | |
| 264 | 4 | page = GenericXLogRegisterBuffer(state, buf, GENERIC_XLOG_FULL_IMAGE); | |
| 265 | 4 | mp = (PrismMetaPage *)PageGetSpecialPointer(page); | |
| 266 | 4 | mp->flags |= PRISM_META_FLAG_FASTSCAN; | |
| 267 | 4 | GenericXLogFinish(state); | |
| 268 | 4 | UnlockReleaseBuffer(buf); | |
| 269 | } | ||
| 270 | 319 | } | |
| 271 | |||
| 272 | /* ---------------------------------------------------------------- | ||
| 273 | * CALL prism_convert_posting_to_fastscan(regclass, int4) | ||
| 274 | * | ||
| 275 | * Converts one cluster's posting chain from AoS to fastscan. | ||
| 276 | * Updates the centroid leaf entry and sets the metadata flag. | ||
| 277 | * Reports the new posting head block number via NOTICE. | ||
| 278 | * ---------------------------------------------------------------- */ | ||
| 279 | Datum | ||
| 280 | 324 | vs_convert_posting_to_fastscan(PG_FUNCTION_ARGS) | |
| 281 | { | ||
| 282 | 324 | reject_null_arg(fcinfo, 0, "index_oid"); | |
| 283 | 324 | reject_null_arg(fcinfo, 1, "cluster_id"); | |
| 284 | |||
| 285 | 324 | Oid indexoid = PG_GETARG_OID(0); | |
| 286 | 324 | int32 cluster_id = PG_GETARG_INT32(1); | |
| 287 | 324 | Relation index = relation_open(indexoid, PRISM_MAINT_LOCK); | |
| 288 | |||
| 289 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 324 times.
|
324 | if (index->rd_rel->relkind != RELKIND_INDEX) |
| 290 | { | ||
| 291 | ✗ | relation_close(index, PRISM_MAINT_LOCK); | |
| 292 | ✗ | ereport(ERROR, | |
| 293 | (errcode(ERRCODE_WRONG_OBJECT_TYPE), | ||
| 294 | errmsg("\"%s\" is not an index", | ||
| 295 | RelationGetRelationName(index)))); | ||
| 296 | } | ||
| 297 | |||
| 298 | 324 | require_index_owner(index, PRISM_MAINT_LOCK); | |
| 299 | |||
| 300 | /* Read metadata */ | ||
| 301 | 322 | Buffer meta_buf = ReadBuffer(index, 0); | |
| 302 | 322 | LockBuffer(meta_buf, BUFFER_LOCK_SHARE); | |
| 303 | 322 | Page meta_page = BufferGetPage(meta_buf); | |
| 304 | |||
| 305 | 322 | const PrismMetaPage *meta = (const PrismMetaPage *)PageGetSpecialPointer( | |
| 306 | meta_page); | ||
| 307 | |||
| 308 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 321 times.
|
322 | if (meta->magic != PRISM_META_MAGIC) |
| 309 | { | ||
| 310 | 1 | UnlockReleaseBuffer(meta_buf); | |
| 311 | 1 | relation_close(index, PRISM_MAINT_LOCK); | |
| 312 |
1/2✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
|
1 | ereport(ERROR, |
| 313 | (errcode(ERRCODE_WRONG_OBJECT_TYPE), | ||
| 314 | errmsg("\"%s\" is not a prism index", | ||
| 315 | RelationGetRelationName(index)))); | ||
| 316 | } | ||
| 317 | |||
| 318 | 321 | BlockNumber first_centroid = meta->first_centroid; | |
| 319 | 321 | Dimension dim = meta->dim; | |
| 320 | 321 | uint8_t nlevels = meta->nlevels; | |
| 321 | 321 | UnlockReleaseBuffer(meta_buf); | |
| 322 | |||
| 323 | /* | ||
| 324 | * Find the leaf for this cluster. The argument is the stored cluster_id -- | ||
| 325 | * what prism_posting_pages reports and what callers pass -- not a | ||
| 326 | * positional index. collect_leaf_entries returns leaves in tree-traversal | ||
| 327 | * order, which coincides with cluster_id only for a single-page flat tree; | ||
| 328 | * on any deeper or multi-page tree the two diverge. Match on each leaf's | ||
| 329 | * own head cluster_id so the argument means the same thing everywhere. | ||
| 330 | */ | ||
| 331 | 321 | LeafEntry *leaves; | |
| 332 | 321 | int nleaves = | |
| 333 | 321 | collect_leaf_entries(index, first_centroid, nlevels, dim, &leaves); | |
| 334 | |||
| 335 | 321 | LeafEntry *leaf = NULL; | |
| 336 |
2/2✓ Branch 1 taken 33921 times.
✓ Branch 2 taken 1 times.
|
33922 | for (int i = 0; i < nleaves; i++) |
| 337 | { | ||
| 338 | 33921 | Buffer hbuf = ReadBuffer(index, leaves[i].posting_head); | |
| 339 | 33921 | LockBuffer(hbuf, BUFFER_LOCK_SHARE); | |
| 340 | 33921 | uint32_t cid = prism_posting_opaque(BufferGetPage(hbuf))->cluster_id; | |
| 341 | 33921 | UnlockReleaseBuffer(hbuf); | |
| 342 |
2/2✓ Branch 0 taken 320 times.
✓ Branch 1 taken 33601 times.
|
33921 | if (cid == (uint32_t)cluster_id) |
| 343 | { | ||
| 344 | 320 | leaf = &leaves[i]; | |
| 345 | 320 | break; | |
| 346 | } | ||
| 347 | } | ||
| 348 | |||
| 349 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 320 times.
|
321 | if (leaf == NULL) |
| 350 | { | ||
| 351 | 1 | pfree(leaves); | |
| 352 | 1 | relation_close(index, PRISM_MAINT_LOCK); | |
| 353 |
1/2✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
|
1 | ereport(ERROR, |
| 354 | (errcode(ERRCODE_INVALID_PARAMETER_VALUE), | ||
| 355 | errmsg("cluster %d not found or has no posting list", | ||
| 356 | cluster_id))); | ||
| 357 | } | ||
| 358 | |||
| 359 | 320 | BlockNumber old_head = leaf->posting_head; | |
| 360 | 320 | BlockNumber centroid_page = leaf->centroid_page; | |
| 361 | 320 | uint16_t entry_idx = leaf->entry_idx; | |
| 362 | 320 | pfree(leaves); | |
| 363 | |||
| 364 | /* Skip if already fastscan */ | ||
| 365 | { | ||
| 366 | 320 | Buffer buf = ReadBuffer(index, old_head); | |
| 367 | 320 | LockBuffer(buf, BUFFER_LOCK_SHARE); | |
| 368 | 320 | Page page = BufferGetPage(buf); | |
| 369 | 320 | PrismPostingPageOpaque *op = prism_posting_opaque(page); | |
| 370 | 320 | bool already_fastscan = (op->flags & PRISM_POSTING_PAGE_FASTSCAN) != 0; | |
| 371 | 320 | UnlockReleaseBuffer(buf); | |
| 372 | |||
| 373 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 319 times.
|
320 | if (already_fastscan) |
| 374 | { | ||
| 375 | 1 | relation_close(index, PRISM_MAINT_LOCK); | |
| 376 |
1/2✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
|
1 | ereport(NOTICE, |
| 377 | (errmsg("cluster %d already fastscan (head block %u)", | ||
| 378 | cluster_id, | ||
| 379 | old_head))); | ||
| 380 | 1 | PG_RETURN_VOID(); | |
| 381 | } | ||
| 382 | } | ||
| 383 | |||
| 384 | /* Convert the posting chain */ | ||
| 385 | 319 | VsPgStorage storage; | |
| 386 | 319 | vs_pg_storage_init(&storage, index, NULL, DISTANCE_L2); | |
| 387 | |||
| 388 | /* | ||
| 389 | * Online conversion, unlike a full index build, has no closing | ||
| 390 | * log_newpage_range() to blanket-WAL the new pages. Keep build_mode off | ||
| 391 | * so each fastscan page is WAL-logged as it is committed (per-page | ||
| 392 | * GenericXLog full image). Otherwise the pages would be dirtied but never | ||
| 393 | * shipped, while the centroid repoint below *is* WAL-logged — leaving a | ||
| 394 | * standby whose centroid points at posting heads it never received. | ||
| 395 | */ | ||
| 396 | 319 | storage.build_mode = false; | |
| 397 | |||
| 398 | 319 | BlockNumber new_head = | |
| 399 | 319 | prism_posting_convert_to_fastscan(&storage.base, old_head, dim); | |
| 400 | |||
| 401 | /* | ||
| 402 | * Publish the new head, but only if a concurrent converter has not | ||
| 403 | * already moved it. If it has, our new_head chain is orphaned and the | ||
| 404 | * winner's head is returned instead. | ||
| 405 | */ | ||
| 406 | 319 | BlockNumber current_head; | |
| 407 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 319 times.
|
319 | if (!update_centroid_posting_head( |
| 408 | index, | ||
| 409 | centroid_page, | ||
| 410 | entry_idx, | ||
| 411 | old_head, | ||
| 412 | new_head, | ||
| 413 | dim, | ||
| 414 | ¤t_head)) | ||
| 415 | { | ||
| 416 | ✗ | relation_close(index, PRISM_MAINT_LOCK); | |
| 417 | ✗ | ereport(NOTICE, | |
| 418 | (errmsg("cluster %d already converted by a concurrent " | ||
| 419 | "call (head block %u)", | ||
| 420 | cluster_id, | ||
| 421 | current_head))); | ||
| 422 | ✗ | PG_RETURN_VOID(); | |
| 423 | } | ||
| 424 | |||
| 425 | 319 | ensure_meta_fastscan_flag(index); | |
| 426 | |||
| 427 | /* | ||
| 428 | * Keep PRISM_MAINT_LOCK until end of transaction (NoLock here releases the | ||
| 429 | * reference, not the lock). maint_end queues a relcache invalidation for | ||
| 430 | * the new leaf count, and that is only delivered at commit -- release the | ||
| 431 | * lock now and the next maintenance call could take it, still holding a | ||
| 432 | * cached index base with the stale count, and mint cluster ids that | ||
| 433 | * collide with the ones just written. | ||
| 434 | */ | ||
| 435 | 319 | relation_close(index, NoLock); | |
| 436 | |||
| 437 |
2/2✓ Branch 1 taken 258 times.
✓ Branch 2 taken 61 times.
|
319 | ereport(NOTICE, |
| 438 | (errmsg("converted cluster %d to fastscan (new head block %u)", | ||
| 439 | cluster_id, | ||
| 440 | new_head))); | ||
| 441 | PG_RETURN_VOID(); | ||
| 442 | } | ||
| 443 | |||
| 444 | /* ---------------------------------------------------------------- | ||
| 445 | * Heap vector fetch for re-clustering (the PrismSplitEnv seam) | ||
| 446 | * ---------------------------------------------------------------- */ | ||
| 447 | |||
| 448 | typedef struct PgSplitFetchCtx | ||
| 449 | { | ||
| 450 | Relation heap; | ||
| 451 | AttrNumber attnum; | ||
| 452 | TupleTableSlot *slot; | ||
| 453 | /* How to read the indexed column as float32 -- the index's own type, not | ||
| 454 | * an assumed one. Carries the conversion buffer, so it outlives a fetch. | ||
| 455 | */ | ||
| 456 | Vec32Access access; | ||
| 457 | /* For the reserve-nlist seam, which has only this context to work from. */ | ||
| 458 | VsStorage *storage; | ||
| 459 | } PgSplitFetchCtx; | ||
| 460 | |||
| 461 | static bool | ||
| 462 | 29412 | pg_split_fetch_vector( | |
| 463 | void *ctx, ItemPointerData tid, float *out, Dimension dim) | ||
| 464 | { | ||
| 465 | 29412 | PgSplitFetchCtx *c = (PgSplitFetchCtx *)ctx; | |
| 466 | |||
| 467 | /* Runs in the walk's per-entry context (see ChainTidCb), which is reset | ||
| 468 | * after every entry -- so the detoast below, and a TOAST reassembly if | ||
| 469 | * the vector is stored out of line, need no cleanup of their own. | ||
| 470 | * | ||
| 471 | * SnapshotAny: the posting entry exists until VACUUM tombstones it, so we | ||
| 472 | * re-cluster against whatever it still points at; a fully-pruned tuple | ||
| 473 | * just drops out of the split (returns false). Same contract as rerank. */ | ||
| 474 |
2/2✓ Branch 1 taken 29400 times.
✓ Branch 2 taken 12 times.
|
29412 | if (!table_tuple_fetch_row_version(c->heap, &tid, SnapshotAny, c->slot)) |
| 475 | return false; | ||
| 476 | |||
| 477 | 29400 | bool isnull; | |
| 478 | 29400 | Datum val = slot_getattr(c->slot, c->attnum, &isnull); | |
| 479 | 29400 | bool ok = false; | |
| 480 |
1/2✓ Branch 0 taken 29400 times.
✗ Branch 1 not taken.
|
29400 | if (!isnull) |
| 481 | { | ||
| 482 | /* | ||
| 483 | * Read through the index's own type descriptor, as the insert and | ||
| 484 | * rerank paths do. Reading the datum as a float32 vector directly | ||
| 485 | * would be wrong for any other indexed type: a vec16 column passes | ||
| 486 | * the shape check this maintenance requires, and its 16-bit payload | ||
| 487 | * read as `dim` floats runs off the end of the value and feeds the | ||
| 488 | * split whatever follows it. | ||
| 489 | * | ||
| 490 | * Detoast explicitly rather than leaving it to the descriptor, so the | ||
| 491 | * copy can be released here. A vector wide enough to be stored out of | ||
| 492 | * line is copied on every fetch, and this runs once per entry of every | ||
| 493 | * list a pass touches -- keeping them would cost the whole pass's | ||
| 494 | * worth of vector data on top of the list being clustered. | ||
| 495 | */ | ||
| 496 | 29400 | struct varlena *raw = (struct varlena *)DatumGetPointer(val); | |
| 497 | 29400 | struct varlena *flat = pg_detoast_datum(raw); | |
| 498 | 29400 | Vec32Ref vref = vec32_read(&c->access, PointerGetDatum(flat)); | |
| 499 | |||
| 500 |
2/2✓ Branch 0 taken 25682 times.
✓ Branch 1 taken 3718 times.
|
29400 | memcpy(out, vref.data, (size_t)dim * sizeof(float)); |
| 501 | 29400 | ok = true; | |
| 502 | |||
| 503 |
2/2✓ Branch 0 taken 25682 times.
✓ Branch 1 taken 3718 times.
|
29400 | if (flat != raw) |
| 504 | 25682 | pfree(flat); | |
| 505 | } | ||
| 506 | 29400 | ExecClearTuple(c->slot); | |
| 507 | 29400 | return ok; | |
| 508 | } | ||
| 509 | |||
| 510 | /* | ||
| 511 | * Retire the split's old chain (the PrismSplitEnv seam). Rather than | ||
| 512 | * tombstoning it now — which would make scans skip a head a concurrent query | ||
| 513 | * may still be about to read from a stale pre-flip pointer — mark each page | ||
| 514 | * DELETED and stamp the head with the current next-XID. The chain stays linked | ||
| 515 | * and readable; VACUUM physically retires it once that XID clears the global | ||
| 516 | * visibility horizon (see vs_rebalance). Scans read DELETED pages | ||
| 517 | * (only TOMBSTONED is skipped), so an in-flight scanner still sees the full | ||
| 518 | * old list. | ||
| 519 | */ | ||
| 520 | /* | ||
| 521 | * Start the read for the heap block holding `tid`, one step ahead of the walk | ||
| 522 | * that is about to fetch it. PrefetchBuffer does nothing when the block is | ||
| 523 | * already resident, so a cached heap pays a buffer-table probe per distinct | ||
| 524 | * block and no I/O; a cold one gets the read started while the current entry | ||
| 525 | * is still being decoded. | ||
| 526 | */ | ||
| 527 | static void | ||
| 528 | 4114 | pg_prefetch_vector(void *ctx, ItemPointerData tid) | |
| 529 | { | ||
| 530 | 4114 | PgSplitFetchCtx *c = (PgSplitFetchCtx *)ctx; | |
| 531 | |||
| 532 | 4114 | PrefetchBuffer(c->heap, MAIN_FORKNUM, ItemPointerGetBlockNumber(&tid)); | |
| 533 | 4114 | } | |
| 534 | |||
| 535 | static void | ||
| 536 | 179 | retire_page(PrismPostingPageOpaque *op, void *state) | |
| 537 | { | ||
| 538 | 179 | op->flags |= PRISM_POSTING_PAGE_DELETED; | |
| 539 | /* delete_xid overlays live_count/tail_blkno, unused once retired. */ | ||
| 540 | 179 | op->delete_xid = *(const uint64 *)state; | |
| 541 | 179 | } | |
| 542 | |||
| 543 | static void | ||
| 544 | 98 | pg_retire_chain(void *ctx, VsStorage *posting_storage, BlockNumber head) | |
| 545 | { | ||
| 546 | 98 | (void)ctx; | |
| 547 | /* | ||
| 548 | * Stamp with this transaction's XID — the one that made the chain | ||
| 549 | * unreachable by committing the centroid flip. Any scan that could still | ||
| 550 | * hold a stale pointer took its snapshot no later than this XID, so once | ||
| 551 | * the global horizon passes it no such scan remains and the chain is safe | ||
| 552 | * to reclaim (the invariant btree page deletion uses). The split has | ||
| 553 | * already written WAL, so an XID is assigned. | ||
| 554 | */ | ||
| 555 | 98 | uint64 dxid = U64FromFullTransactionId(GetTopFullTransactionId()); | |
| 556 | |||
| 557 | 98 | prism_posting_chain_mutate(posting_storage, head, retire_page, &dxid); | |
| 558 | 98 | } | |
| 559 | |||
| 560 | /* ---------------------------------------------------------------- | ||
| 561 | * Helpers | ||
| 562 | * ---------------------------------------------------------------- */ | ||
| 563 | |||
| 564 | /* Persist an updated leaf count into the metapage (block 0), in place. */ | ||
| 565 | static void | ||
| 566 | 124 | persist_nlist(VsStorage *storage, uint32_t nlist) | |
| 567 | { | ||
| 568 | 124 | Page page = vs_storage_write_page(storage, 0); | |
| 569 | 124 | PrismMetaPage *meta = (PrismMetaPage *)PageGetSpecialPointer(page); | |
| 570 | 124 | meta->nlist = nlist; | |
| 571 | 124 | vs_storage_commit_page(storage, 0); | |
| 572 | 124 | } | |
| 573 | |||
| 574 | /* | ||
| 575 | * Persist the centroid page count (block 0), in place. | ||
| 576 | * | ||
| 577 | * A split that runs out of room on a level-0 centroid page extends the | ||
| 578 | * relation, so the count cannot be recovered from the block layout | ||
| 579 | * afterwards -- see PrismMetaPage.ncentroid_pages. Written once per | ||
| 580 | * maintenance pass rather than per split, alongside the leaf count. | ||
| 581 | * | ||
| 582 | * Losing this write cannot corrupt anything: nothing reads the count to | ||
| 583 | * find a page. It would leave the planner's descent term low until the next | ||
| 584 | * pass. | ||
| 585 | */ | ||
| 586 | static void | ||
| 587 | 26 | persist_ncentroid_pages(VsStorage *storage, uint32_t ncentroid_pages) | |
| 588 | { | ||
| 589 | 26 | Page page = vs_storage_write_page(storage, 0); | |
| 590 | 26 | PrismMetaPage *meta = (PrismMetaPage *)PageGetSpecialPointer(page); | |
| 591 | 26 | meta->ncentroid_pages = ncentroid_pages; | |
| 592 | 26 | vs_storage_commit_page(storage, 0); | |
| 593 | 26 | } | |
| 594 | |||
| 595 | /* | ||
| 596 | * Persist the leaf count, so the ids counted from it survive a crash that | ||
| 597 | * leaves the new leaves reachable -- see PrismSplitEnv.reserve_nlist. The | ||
| 598 | * storage handle is reached through the split's fetch context, which is the | ||
| 599 | * only context the seam carries. | ||
| 600 | */ | ||
| 601 | static void | ||
| 602 | 98 | pg_reserve_nlist(void *ctx, uint32_t nlist) | |
| 603 | { | ||
| 604 | 98 | PgSplitFetchCtx *c = (PgSplitFetchCtx *)ctx; | |
| 605 | |||
| 606 | 98 | persist_nlist(c->storage, nlist); | |
| 607 | 98 | } | |
| 608 | |||
| 609 | static void | ||
| 610 | 42 | require_supported_shape(Relation index, PrismIndexBase *base) | |
| 611 | { | ||
| 612 |
2/2✓ Branch 0 taken 41 times.
✓ Branch 1 taken 1 times.
|
42 | if (base->nlevels != 1 || |
| 613 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 40 times.
|
41 | base->centroid_format != PRISM_CENTROID_FMT_RABITQ) |
| 614 | { | ||
| 615 | 2 | char *name = pstrdup(RelationGetRelationName(index)); | |
| 616 |
1/2✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
|
2 | ereport(ERROR, |
| 617 | (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), | ||
| 618 | errmsg("incremental split is not supported for index \"%s\"", | ||
| 619 | name), | ||
| 620 | errdetail( | ||
| 621 | "Only flat (single-level) indexes with RaBitQ " | ||
| 622 | "centroid pages are supported in this release."))); | ||
| 623 | } | ||
| 624 | 40 | } | |
| 625 | |||
| 626 | /* | ||
| 627 | * The memory a split may use, from maintenance_work_mem -- | ||
| 628 | * the budget an operator already raises for index work, and the same knob the | ||
| 629 | * bulk build sizes its clustering sample from. A split streams the list, so | ||
| 630 | * this bounds its memory whatever the list's size -- see | ||
| 631 | * prism_split_sample_cap, which turns the budget into a sample size after | ||
| 632 | * reserving what clustering costs alongside it. | ||
| 633 | */ | ||
| 634 | static uint64_t | ||
| 635 | 138 | maint_memory_budget(void) | |
| 636 | { | ||
| 637 | 138 | return (uint64_t)maintenance_work_mem * UINT64CONST(1024); | |
| 638 | } | ||
| 639 | |||
| 640 | /* | ||
| 641 | * Refuse the pass if the budget cannot pay for a split at this dimension, | ||
| 642 | * rather than quietly exceeding it. Checked once per pass: the answer depends | ||
| 643 | * only on the dimension and the setting, so it cannot change list by list. | ||
| 644 | */ | ||
| 645 | static void | ||
| 646 | 40 | require_memory_budget(Relation index, Dimension dim) | |
| 647 | { | ||
| 648 |
2/2✓ Branch 0 taken 39 times.
✓ Branch 1 taken 1 times.
|
40 | if (prism_split_sample_cap(maint_memory_budget(), dim) > 0) |
| 649 | 39 | return; | |
| 650 | |||
| 651 | 1 | uint64 need = prism_split_min_budget_bytes(dim); | |
| 652 | 1 | char *name = pstrdup(RelationGetRelationName(index)); | |
| 653 | |||
| 654 |
1/2✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
|
40 | ereport(ERROR, |
| 655 | (errcode(ERRCODE_INSUFFICIENT_RESOURCES), | ||
| 656 | errmsg("maintenance_work_mem is too small to split a posting " | ||
| 657 | "list of index \"%s\"", | ||
| 658 | name), | ||
| 659 | errdetail( | ||
| 660 | "Splitting %u-dimension vectors needs at " | ||
| 661 | "least " UINT64_FORMAT | ||
| 662 | " kB, but maintenance_work_mem is %d kB.", | ||
| 663 | (uint32)dim, | ||
| 664 | (need + 1023) / 1024, | ||
| 665 | maintenance_work_mem), | ||
| 666 | errhint("Increase maintenance_work_mem and retry."))); | ||
| 667 | } | ||
| 668 | |||
| 669 | /* | ||
| 670 | * Is this posting page a live chain head -- reachable, not retired, not all | ||
| 671 | * dead? Written the same way wherever it is asked (the maintenance scan, the | ||
| 672 | * re-read under the head lock, vacuum's head recognition), so the three cannot | ||
| 673 | * drift. | ||
| 674 | */ | ||
| 675 | static bool | ||
| 676 | 100 | posting_head_is_live(const PrismPostingPageOpaque *op) | |
| 677 | { | ||
| 678 | 100 | return (op->flags & PRISM_POSTING_PAGE_FIRST) && | |
| 679 |
2/2✓ Branch 0 taken 98 times.
✓ Branch 1 taken 2 times.
|
100 | !(op->flags & PRISM_POSTING_PAGE_TOMBSTONED) && |
| 680 | !(op->flags & PRISM_POSTING_PAGE_DELETED); | ||
| 681 | } | ||
| 682 | |||
| 683 | /* | ||
| 684 | * Split the posting head at `head` if it is a live first page and, when | ||
| 685 | * target > 0, holds more entries than the split trigger | ||
| 686 | * (prism_split_trigger). Serialized against inserts to the same | ||
| 687 | * cluster by the head page lock. Returns true and fills *res if a split | ||
| 688 | * happened. | ||
| 689 | * | ||
| 690 | * The live_count test here is only a cheap early-out on a page already read. | ||
| 691 | * The authoritative check is inside prism_posting_split, against the entry | ||
| 692 | * count after collection: live_count does not account for entries whose vector | ||
| 693 | * can no longer be fetched, so a list can look oversized here and turn out not | ||
| 694 | * to be. | ||
| 695 | * | ||
| 696 | * target also sets the width -- round(count / target) parts, so each new list | ||
| 697 | * rests at the target with room to grow back to the trigger. With target == 0 | ||
| 698 | * the split is unconditional and 2-way: the manual escape hatch. | ||
| 699 | */ | ||
| 700 | static bool | ||
| 701 | 100 | split_one_head( | |
| 702 | Relation index, | ||
| 703 | PrismIndexBase *base, | ||
| 704 | PgSplitFetchCtx *fc, | ||
| 705 | BlockNumber head, | ||
| 706 | uint32_t target, | ||
| 707 | PrismSplitResult *res) | ||
| 708 | { | ||
| 709 | 100 | LockPage(index, head, ExclusiveLock); | |
| 710 | |||
| 711 | 100 | Page p = vs_storage_read_page(base->posting_storage, head); | |
| 712 |
1/2✓ Branch 1 taken 100 times.
✗ Branch 2 not taken.
|
100 | bool live_head = prism_page_is_posting(p) && |
| 713 |
1/2✓ Branch 0 taken 100 times.
✗ Branch 1 not taken.
|
100 | posting_head_is_live(prism_posting_opaque(p)); |
| 714 | /* live_count is meaningful only when the head is live: delete_xid overlays | ||
| 715 | * it once DELETED, which posting_head_is_live excludes. */ | ||
| 716 | 98 | uint32_t live = live_head ? prism_posting_opaque(p)->live_count : 0; | |
| 717 | 100 | vs_storage_release_page(base->posting_storage, head); | |
| 718 | |||
| 719 |
4/4✓ Branch 0 taken 98 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 95 times.
✓ Branch 3 taken 3 times.
|
100 | if (!live_head || |
| 720 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 95 times.
|
95 | (target > 0 && (uint64_t)live <= prism_split_trigger(target))) |
| 721 | { | ||
| 722 | 2 | UnlockPage(index, head, ExclusiveLock); | |
| 723 | 2 | return false; | |
| 724 | } | ||
| 725 | |||
| 726 | 98 | PrismSplitEnv env = { | |
| 727 | .fetch_vector = pg_split_fetch_vector, | ||
| 728 | .prefetch_vector = pg_prefetch_vector, | ||
| 729 | .retire_chain = pg_retire_chain, | ||
| 730 | .reserve_nlist = pg_reserve_nlist, | ||
| 731 | .ctx = fc, | ||
| 732 | }; | ||
| 733 | /* | ||
| 734 | * Test hook: fires holding the old head's page lock, with the centroid | ||
| 735 | * leaf still pointing at it. That is the window an isolation test needs -- | ||
| 736 | * an insert routed now reaches this head, blocks on the page lock, and | ||
| 737 | * once the split below has flipped the leaf and retired the chain it wakes | ||
| 738 | * to find the head retired and has to route again. Firing after the split | ||
| 739 | * instead would prove nothing: the leaf would already point elsewhere, so | ||
| 740 | * the insert would route straight to a new head and never contend. | ||
| 741 | * | ||
| 742 | * No-op unless PG was built with injection points and a test attached an | ||
| 743 | * action. | ||
| 744 | */ | ||
| 745 | 98 | INJECTION_POINT("prism-split-locked", NULL); | |
| 746 | |||
| 747 | 98 | PrismSplitConfig cfg = { | |
| 748 | .target_entries = target, | ||
| 749 | .sample_budget_bytes = maint_memory_budget(), | ||
| 750 | }; | ||
| 751 | 98 | int rc = prism_posting_split(base, head, &cfg, &env, res); | |
| 752 | |||
| 753 | 98 | UnlockPage(index, head, ExclusiveLock); | |
| 754 | |||
| 755 |
2/4✓ Branch 0 taken 98 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 98 times.
|
98 | return rc == 0 && res->did_split; |
| 756 | } | ||
| 757 | |||
| 758 | /* | ||
| 759 | * Resolve the resting list size maintenance should aim at. | ||
| 760 | * | ||
| 761 | * Derived from the row count alone, deliberately ignoring the nlist | ||
| 762 | * reloption. Above the dimension's per-list target squared that is the target | ||
| 763 | * itself; below it the sqrt floor in prism_auto_nlist makes it smaller, which | ||
| 764 | * is the regime where a hardcoded target would fight the build and merge a | ||
| 765 | * small index down to too few lists. | ||
| 766 | * | ||
| 767 | * It used to honour an explicit nlist instead, on the grounds that a | ||
| 768 | * rebalanced index should keep the shape the build chose -- the cost model | ||
| 769 | * and the automatic nprobe both key off nlist. That is a real property and | ||
| 770 | * this gives it up: re-partitioning changes how many lists a query probes. | ||
| 771 | * | ||
| 772 | * What it bought was worse. With target = rows / nlist the target scales with | ||
| 773 | * the table, so an index built with an explicit nlist never splits on growth | ||
| 774 | * at all: lists grow in proportion, the trigger is never reached, and a pass | ||
| 775 | * correctly reports splitting nothing while every probe scans more entries | ||
| 776 | * than the last time. A number typed once at CREATE INDEX would silently | ||
| 777 | * switch off maintenance for the life of the index -- and once maintenance is | ||
| 778 | * VACUUM-driven, nothing would ever revisit it. | ||
| 779 | * | ||
| 780 | * A list should rest at the size that keeps a probe's cost flat, which is | ||
| 781 | * what prism_target_entries_per_dim is for. nlist stays what the user asked | ||
| 782 | * the *build* for; it is not a maintenance policy. | ||
| 783 | * | ||
| 784 | * target_pages, by contrast, IS read here, from the index's current | ||
| 785 | * reloptions rather than from whatever the build saw. It names the resting | ||
| 786 | * shape directly, so ALTER INDEX ... SET (target_pages = ...) followed by | ||
| 787 | * rebalance() is the supported way to re-partition an index without | ||
| 788 | * rebuilding it -- deliberate, unlike the nlist case above, where honouring | ||
| 789 | * the option silently switched maintenance off. | ||
| 790 | */ | ||
| 791 | static uint32_t | ||
| 792 | 4 | resolve_target_entries(Relation heap, Relation index, Dimension dim) | |
| 793 | { | ||
| 794 | 4 | const PrismOptions *opts = (const PrismOptions *)index->rd_options; | |
| 795 |
1/2✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
|
4 | uint32_t tpages = (opts != NULL) ? (uint32_t)opts->target_pages : 0; |
| 796 | |||
| 797 | 4 | return prism_target_entries_per_list( | |
| 798 | prism_estimate_heap_tuples(heap), 0, dim, tpages); | ||
| 799 | } | ||
| 800 | |||
| 801 | /* Common setup: base + storage + heap fetch context. */ | ||
| 802 | typedef struct MaintCtx | ||
| 803 | { | ||
| 804 | PrismIndexBase base; | ||
| 805 | VsPgStorage storage; | ||
| 806 | Relation heap; | ||
| 807 | PgSplitFetchCtx fetch; | ||
| 808 | ResourceOwner params_owner; | ||
| 809 | /* | ||
| 810 | * Scratch for one list's split. Splitting a list holds every one of its | ||
| 811 | * vectors at full precision, so the peak is inherent -- but a pass over | ||
| 812 | * many lists must not stack those peaks. Reset between lists (see | ||
| 813 | * maint_split_done) so the cost is one list's worth, not the pass's. | ||
| 814 | */ | ||
| 815 | MemoryContext split_ctx; | ||
| 816 | } MaintCtx; | ||
| 817 | |||
| 818 | static void | ||
| 819 | 42 | maint_begin(Relation index, MaintCtx *m) | |
| 820 | { | ||
| 821 | 42 | m->params_owner = CurrentResourceOwner; | |
| 822 | 42 | prism_index_base_init(index, &m->base); | |
| 823 | 42 | require_supported_shape(index, &m->base); | |
| 824 | |||
| 825 | 40 | vs_pg_storage_init(&m->storage, index, NULL, m->base.metric); | |
| 826 | 40 | m->base.centroid_storage = &m->storage.base; | |
| 827 | 40 | m->base.posting_storage = &m->storage.base; | |
| 828 | 40 | m->base.page_base = NULL; | |
| 829 | |||
| 830 | 40 | m->heap = table_open(index->rd_index->indrelid, AccessShareLock); | |
| 831 | 40 | m->fetch.heap = m->heap; | |
| 832 | 40 | m->fetch.attnum = index->rd_index->indkey.values[0]; | |
| 833 | 40 | m->fetch.slot = table_slot_create(m->heap, NULL); | |
| 834 | 40 | m->fetch.access = vec32_access( | |
| 835 | 40 | prism_cache_type_info(index), m->base.dim, CurrentMemoryContext); | |
| 836 | 40 | m->fetch.storage = &m->storage.base; | |
| 837 | |||
| 838 | /* Created here, but not switched into: everything above has to outlive the | ||
| 839 | * per-list resets. */ | ||
| 840 | 40 | m->split_ctx = AllocSetContextCreate( | |
| 841 | CurrentMemoryContext, "prism split", ALLOCSET_DEFAULT_SIZES); | ||
| 842 | 40 | } | |
| 843 | |||
| 844 | /* | ||
| 845 | * Run one list's split with its scratch accounted separately, and release that | ||
| 846 | * scratch before returning. Splitting is where a maintenance pass allocates, | ||
| 847 | * and one list is the logical point at which none of it is needed any more: | ||
| 848 | * the result is returned by value and the index state the caller keeps reading | ||
| 849 | * lives in the pass's own context. | ||
| 850 | */ | ||
| 851 | static bool | ||
| 852 | 100 | split_one_head_in_scratch( | |
| 853 | Relation index, | ||
| 854 | MaintCtx *m, | ||
| 855 | BlockNumber head, | ||
| 856 | uint32_t target, | ||
| 857 | PrismSplitResult *res) | ||
| 858 | { | ||
| 859 | 100 | MemoryContext old = MemoryContextSwitchTo(m->split_ctx); | |
| 860 | 100 | bool did = split_one_head(index, &m->base, &m->fetch, head, target, res); | |
| 861 | 100 | MemoryContextSwitchTo(old); | |
| 862 | 100 | MemoryContextReset(m->split_ctx); | |
| 863 | 100 | return did; | |
| 864 | } | ||
| 865 | |||
| 866 | /* | ||
| 867 | * Drop the nlist reloption once maintenance has moved the leaf count away | ||
| 868 | * from it. | ||
| 869 | * | ||
| 870 | * A split restructures the index the way an ALTER plus REINDEX would, only | ||
| 871 | * incrementally and without the exclusive lock -- so the declaration has to | ||
| 872 | * stop claiming a width the index no longer has. Otherwise a later REINDEX | ||
| 873 | * rebuilds at the number typed at CREATE INDEX: an index built with | ||
| 874 | * nlist = 100, grown a hundredfold and split to match, comes back with 100 | ||
| 875 | * lists, and no user could reasonably be expected to ALTER the right value in | ||
| 876 | * beforehand. | ||
| 877 | * | ||
| 878 | * Cleared rather than set to the new count. Writing 10000 in would assert the | ||
| 879 | * user asked for exactly 10000, which they did not, and it would be a fresh | ||
| 880 | * pin that goes stale on the next growth cycle. Removing it lets every later | ||
| 881 | * rebuild derive from the rows as they then are. | ||
| 882 | * | ||
| 883 | * Only when the reloption is set. Left out, auto-derivation already lands | ||
| 884 | * where the splits were heading, and writing anything in would convert "size | ||
| 885 | * this for my data" into a permanent pin. | ||
| 886 | * | ||
| 887 | * The tuple lock is required, not decoration: reloptions live in a pg_class | ||
| 888 | * column that is also updated in place (ANALYZE's reltuples, say), so a plain | ||
| 889 | * heap_update here can lose a concurrent inplace update. PostgreSQL warns | ||
| 890 | * about exactly that -- "missing lock for relation ... @ TID" -- and holding | ||
| 891 | * AccessExclusiveLock on the index does not satisfy it, because the lock it | ||
| 892 | * wants is on the catalog row. The index-level lock the pass already holds | ||
| 893 | * (PRISM_MAINT_LOCK, ShareUpdateExclusiveLock) is what ALTER INDEX ... SET | ||
| 894 | * takes, so no escalation is needed for the index itself. | ||
| 895 | */ | ||
| 896 | static void | ||
| 897 | 26 | clear_nlist_reloption(Relation index) | |
| 898 | { | ||
| 899 | 26 | PrismOptions *opts = (PrismOptions *)index->rd_options; | |
| 900 | |||
| 901 |
3/4✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 22 times.
|
26 | if (opts == NULL || opts->nlist <= 0) |
| 902 | 4 | return; /* never declared: nothing to clear */ | |
| 903 | |||
| 904 | 22 | Relation pgclass = table_open(RelationRelationId, RowExclusiveLock); | |
| 905 | 22 | HeapTuple tuple = SearchSysCacheCopy1( | |
| 906 | RELOID, ObjectIdGetDatum(RelationGetRelid(index))); | ||
| 907 | |||
| 908 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
|
22 | if (!HeapTupleIsValid(tuple)) |
| 909 | ✗ | elog(ERROR, | |
| 910 | "cache lookup failed for index %u", | ||
| 911 | RelationGetRelid(index)); | ||
| 912 | |||
| 913 | 22 | bool isnull; | |
| 914 | 22 | Datum old = | |
| 915 | 22 | SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, &isnull); | |
| 916 | 22 | List *reset = list_make1(makeDefElem(pstrdup("nlist"), NULL, -1)); | |
| 917 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
44 | Datum newopts = transformRelOptions( |
| 918 | isnull ? (Datum)0 : old, reset, NULL, NULL, false, true); | ||
| 919 | |||
| 920 | 22 | Datum repl_val[Natts_pg_class] = {0}; | |
| 921 | 22 | bool repl_null[Natts_pg_class] = {false}; | |
| 922 | 22 | bool repl_repl[Natts_pg_class] = {false}; | |
| 923 | |||
| 924 |
1/2✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
|
22 | if (DatumGetPointer(newopts) != NULL) |
| 925 | 22 | repl_val[Anum_pg_class_reloptions - 1] = newopts; | |
| 926 | else | ||
| 927 | ✗ | repl_null[Anum_pg_class_reloptions - 1] = true; | |
| 928 | 22 | repl_repl[Anum_pg_class_reloptions - 1] = true; | |
| 929 | |||
| 930 | 22 | HeapTuple newtuple = heap_modify_tuple( | |
| 931 | tuple, RelationGetDescr(pgclass), repl_val, repl_null, repl_repl); | ||
| 932 | |||
| 933 | /* | ||
| 934 | * Keep the old tid: CatalogTupleUpdate rewrites newtuple->t_self to where | ||
| 935 | * the new version lands, so unlocking through it would release a lock on | ||
| 936 | * the wrong row and leave this one held at commit. | ||
| 937 | */ | ||
| 938 | 22 | ItemPointerData otid = newtuple->t_self; | |
| 939 | |||
| 940 | 22 | LockTuple(pgclass, &otid, InplaceUpdateTupleLock); | |
| 941 | 22 | CatalogTupleUpdate(pgclass, &otid, newtuple); | |
| 942 | 22 | UnlockTuple(pgclass, &otid, InplaceUpdateTupleLock); | |
| 943 | |||
| 944 | 22 | heap_freetuple(newtuple); | |
| 945 | 22 | heap_freetuple(tuple); | |
| 946 | 22 | table_close(pgclass, RowExclusiveLock); | |
| 947 | } | ||
| 948 | |||
| 949 | static void | ||
| 950 | 39 | maint_end(Relation index, MaintCtx *m, bool changed) | |
| 951 | { | ||
| 952 |
2/2✓ Branch 0 taken 26 times.
✓ Branch 1 taken 13 times.
|
39 | if (changed) |
| 953 | { | ||
| 954 | 26 | persist_nlist(&m->storage.base, m->base.nlist); | |
| 955 | 26 | persist_ncentroid_pages(&m->storage.base, m->base.ncentroid_pages); | |
| 956 | /* The declaration no longer describes the index -- see | ||
| 957 | * clear_nlist_reloption. */ | ||
| 958 | 26 | clear_nlist_reloption(index); | |
| 959 | /* Drop the cached metapage so later queries see the new leaf count. */ | ||
| 960 | 26 | CacheInvalidateRelcache(index); | |
| 961 | } | ||
| 962 | 39 | MemoryContextDelete(m->split_ctx); | |
| 963 | 39 | ExecDropSingleTupleTableSlot(m->fetch.slot); | |
| 964 | 39 | table_close(m->heap, AccessShareLock); | |
| 965 | 39 | prism_release_params(m->base.dim, m->base.rabitq_seed, m->params_owner); | |
| 966 | 39 | } | |
| 967 | |||
| 968 | /* ---------------------------------------------------------------- | ||
| 969 | * SQL entry points | ||
| 970 | * ---------------------------------------------------------------- */ | ||
| 971 | |||
| 972 | /* | ||
| 973 | * CALL prism_split_posting_list(index regclass, head_blkno bigint) | ||
| 974 | * | ||
| 975 | * Split the single posting list whose head page is head_blkno. Reports via | ||
| 976 | * NOTICE whether a split happened or was declined (not a live head, too few | ||
| 977 | * entries, or degenerate data). | ||
| 978 | */ | ||
| 979 | Datum | ||
| 980 | 9 | vs_split_posting_list(PG_FUNCTION_ARGS) | |
| 981 | { | ||
| 982 | 9 | require_own_transaction(fcinfo, "prism_split_posting_list()"); | |
| 983 | 9 | reject_null_arg(fcinfo, 0, "index_oid"); | |
| 984 | 8 | reject_null_arg(fcinfo, 1, "head_blkno"); | |
| 985 | |||
| 986 | 7 | Oid indexoid = PG_GETARG_OID(0); | |
| 987 | 7 | int64 blk64 = PG_GETARG_INT64(1); | |
| 988 | 7 | Relation index = relation_open(indexoid, PRISM_MAINT_LOCK); | |
| 989 | |||
| 990 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 6 times.
|
7 | if (index->rd_rel->relkind != RELKIND_INDEX) |
| 991 | { | ||
| 992 | 1 | relation_close(index, PRISM_MAINT_LOCK); | |
| 993 |
1/2✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
|
1 | ereport(ERROR, |
| 994 | (errcode(ERRCODE_WRONG_OBJECT_TYPE), | ||
| 995 | errmsg("\"%s\" is not an index", | ||
| 996 | RelationGetRelationName(index)))); | ||
| 997 | } | ||
| 998 | 6 | require_index_owner(index, PRISM_MAINT_LOCK); | |
| 999 | |||
| 1000 | 5 | BlockNumber nblocks = RelationGetNumberOfBlocks(index); | |
| 1001 |
2/4✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
|
5 | if (blk64 < 1 || blk64 >= (int64)nblocks) |
| 1002 | { | ||
| 1003 | ✗ | relation_close(index, PRISM_MAINT_LOCK); | |
| 1004 | ✗ | ereport(ERROR, | |
| 1005 | (errcode(ERRCODE_INVALID_PARAMETER_VALUE), | ||
| 1006 | errmsg("block number " INT64_FORMAT " out of range", blk64))); | ||
| 1007 | } | ||
| 1008 | |||
| 1009 | 5 | MaintCtx m; | |
| 1010 | 5 | maint_begin(index, &m); | |
| 1011 | 5 | require_memory_budget(index, m.base.dim); | |
| 1012 | |||
| 1013 | 5 | PrismSplitResult res; | |
| 1014 | 5 | bool did = | |
| 1015 | 5 | split_one_head_in_scratch(index, &m, (BlockNumber)blk64, 0, &res); | |
| 1016 | |||
| 1017 | 5 | maint_end(index, &m, did); | |
| 1018 | /* | ||
| 1019 | * Keep PRISM_MAINT_LOCK until end of transaction (NoLock here releases the | ||
| 1020 | * reference, not the lock). maint_end queues a relcache invalidation for | ||
| 1021 | * the new leaf count, and that is only delivered at commit -- release the | ||
| 1022 | * lock now and the next maintenance call could take it, still holding a | ||
| 1023 | * cached index base with the stale count, and mint cluster ids that | ||
| 1024 | * collide with the ones just written. | ||
| 1025 | */ | ||
| 1026 | 5 | relation_close(index, NoLock); | |
| 1027 | |||
| 1028 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2 times.
|
5 | if (did) |
| 1029 |
1/2✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
|
3 | ereport(NOTICE, |
| 1030 | (errmsg("split posting list at block " INT64_FORMAT | ||
| 1031 | " into %u lists", | ||
| 1032 | blk64, | ||
| 1033 | res.nparts))); | ||
| 1034 | else | ||
| 1035 | /* | ||
| 1036 | * split_one_head declines for several reasons -- not a live head, too | ||
| 1037 | * few entries, or degenerate data yielding an empty k-means cluster -- | ||
| 1038 | * so report the outcome without guessing which. | ||
| 1039 | */ | ||
| 1040 |
1/2✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
|
2 | ereport(NOTICE, |
| 1041 | (errmsg("posting list at block " INT64_FORMAT " not split", | ||
| 1042 | blk64))); | ||
| 1043 | |||
| 1044 | 5 | PG_RETURN_VOID(); | |
| 1045 | } | ||
| 1046 | |||
| 1047 | /* ---------------------------------------------------------------- | ||
| 1048 | * CALL prism_rebalance(index regclass, target_entries int4 DEFAULT NULL) | ||
| 1049 | * | ||
| 1050 | * Manual LIRE rebalancing entry point. Scans the index and splits every live | ||
| 1051 | * posting-list head that has grown past the split trigger into lists of about | ||
| 1052 | * target_entries entries each, and physically retires any already-DELETED old | ||
| 1053 | * split chain whose delete_xid has cleared the global visibility horizon. | ||
| 1054 | * Reports the number of lists split via NOTICE. New heads created during the | ||
| 1055 | * pass land past the snapshotted block count and are left for a later call. | ||
| 1056 | * | ||
| 1057 | * target_entries is the size a list *rests* at, not a bound it never crosses. | ||
| 1058 | * A list is left alone until it reaches target_entries * | ||
| 1059 | * PRISM_SPLIT_TRIGGER_FACTOR, so the operating band is | ||
| 1060 | * [target/factor, target*factor] with the target at its centre: room to absorb | ||
| 1061 | * inserts and deletes, and room for the unevenness of a k-means split. | ||
| 1062 | * Pinning the trigger at the target instead | ||
| 1063 | * would leave every fresh list one insert away from splitting again. | ||
| 1064 | * | ||
| 1065 | * NULL derives the target from the index itself (see resolve_target_entries), | ||
| 1066 | * which is what an operator should almost always want; passing a value is an | ||
| 1067 | * override. | ||
| 1068 | * | ||
| 1069 | * Runs in a single transaction, so a chain this pass retires does not become | ||
| 1070 | * reclaimable within it: the pass's own XID cannot clear the visibility | ||
| 1071 | * horizon while it is still running. Reclaiming those chains therefore falls | ||
| 1072 | * to a subsequent call. | ||
| 1073 | * | ||
| 1074 | * Splitting is the only rebalancing performed here, and it is driven by the | ||
| 1075 | * caller -- nothing schedules it. | ||
| 1076 | * ---------------------------------------------------------------- */ | ||
| 1077 | Datum | ||
| 1078 | 42 | vs_rebalance(PG_FUNCTION_ARGS) | |
| 1079 | { | ||
| 1080 | 42 | require_own_transaction(fcinfo, "prism_rebalance()"); | |
| 1081 | 40 | reject_null_arg(fcinfo, 0, "index_oid"); | |
| 1082 | |||
| 1083 |
2/2✓ Branch 0 taken 34 times.
✓ Branch 1 taken 5 times.
|
39 | Oid indexoid = PG_GETARG_OID(0); |
| 1084 | /* SQL arg is integer (int4). Unlike the index, a null target is | ||
| 1085 | * meaningful: it asks for the size to be derived from the index. */ | ||
| 1086 | 39 | bool target_given = !PG_ARGISNULL(1); | |
| 1087 |
2/2✓ Branch 0 taken 34 times.
✓ Branch 1 taken 5 times.
|
39 | int32 target_arg = target_given ? PG_GETARG_INT32(1) : 0; |
| 1088 | 39 | Relation index = relation_open(indexoid, PRISM_MAINT_LOCK); | |
| 1089 | |||
| 1090 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 39 times.
|
39 | if (index->rd_rel->relkind != RELKIND_INDEX) |
| 1091 | { | ||
| 1092 | ✗ | relation_close(index, PRISM_MAINT_LOCK); | |
| 1093 | ✗ | ereport(ERROR, | |
| 1094 | (errcode(ERRCODE_WRONG_OBJECT_TYPE), | ||
| 1095 | errmsg("\"%s\" is not an index", | ||
| 1096 | RelationGetRelationName(index)))); | ||
| 1097 | } | ||
| 1098 | 39 | require_index_owner(index, PRISM_MAINT_LOCK); | |
| 1099 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 37 times.
|
38 | if (target_given && target_arg < 1) |
| 1100 | { | ||
| 1101 | 1 | relation_close(index, PRISM_MAINT_LOCK); | |
| 1102 |
1/2✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
|
1 | ereport(ERROR, |
| 1103 | (errcode(ERRCODE_INVALID_PARAMETER_VALUE), | ||
| 1104 | errmsg("target_entries must be positive"))); | ||
| 1105 | } | ||
| 1106 | |||
| 1107 | 37 | MaintCtx m; | |
| 1108 | 37 | maint_begin(index, &m); | |
| 1109 | 35 | require_memory_budget(index, m.base.dim); | |
| 1110 | |||
| 1111 | 68 | uint32_t target = | |
| 1112 | target_given ? (uint32_t)target_arg | ||
| 1113 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 30 times.
|
34 | : resolve_target_entries(m.heap, index, m.base.dim); |
| 1114 | 34 | uint64_t trigger = prism_split_trigger(target); | |
| 1115 | |||
| 1116 | 34 | BlockNumber nblocks = RelationGetNumberOfBlocks(index); | |
| 1117 | 34 | BlockNumber start = Max(m.base.first_posting, 1); | |
| 1118 | 34 | int32 nsplits = 0; | |
| 1119 | 34 | int32 nreclaimed = 0; | |
| 1120 | |||
| 1121 |
2/2✓ Branch 0 taken 332 times.
✓ Branch 1 taken 34 times.
|
366 | for (BlockNumber blk = start; blk < nblocks; blk++) |
| 1122 | { | ||
| 1123 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 332 times.
|
332 | CHECK_FOR_INTERRUPTS(); |
| 1124 | |||
| 1125 | 332 | Page p = vs_storage_read_page(&m.storage.base, blk); | |
| 1126 | 332 | bool posting = prism_page_is_posting(p); | |
| 1127 | 986 | const PrismPostingPageOpaque *op = posting ? prism_posting_opaque(p) | |
| 1128 |
2/2✓ Branch 0 taken 322 times.
✓ Branch 1 taken 10 times.
|
332 | : NULL; |
| 1129 | |||
| 1130 |
2/2✓ Branch 0 taken 216 times.
✓ Branch 1 taken 106 times.
|
322 | bool is_head = posting && (op->flags & PRISM_POSTING_PAGE_FIRST) && |
| 1131 | !(op->flags & PRISM_POSTING_PAGE_TOMBSTONED); | ||
| 1132 |
2/2✓ Branch 0 taken 193 times.
✓ Branch 1 taken 23 times.
|
216 | bool retired = is_head && (op->flags & PRISM_POSTING_PAGE_DELETED); |
| 1133 |
2/2✓ Branch 0 taken 193 times.
✓ Branch 1 taken 139 times.
|
332 | bool candidate = is_head && !retired && |
| 1134 |
2/2✓ Branch 0 taken 98 times.
✓ Branch 1 taken 95 times.
|
193 | (uint64_t)op->live_count > trigger; |
| 1135 | /* delete_xid overlays live_count and is meaningful only when DELETED. | ||
| 1136 | */ | ||
| 1137 |
2/2✓ Branch 0 taken 23 times.
✓ Branch 1 taken 309 times.
|
332 | uint64 dxid = retired ? op->delete_xid : 0; |
| 1138 | 332 | vs_storage_release_page(&m.storage.base, blk); | |
| 1139 | |||
| 1140 |
2/2✓ Branch 0 taken 95 times.
✓ Branch 1 taken 237 times.
|
332 | if (candidate) |
| 1141 | { | ||
| 1142 | /* Pass the target so split_one_head re-checks under the head | ||
| 1143 | * lock and prism_posting_split re-checks again after collection -- | ||
| 1144 | * the list may have shrunk since the unlocked read above, and | ||
| 1145 | * live_count does not see unfetchable entries at all. */ | ||
| 1146 | 95 | PrismSplitResult res; | |
| 1147 |
1/2✓ Branch 1 taken 95 times.
✗ Branch 2 not taken.
|
95 | if (split_one_head_in_scratch(index, &m, blk, target, &res)) |
| 1148 | 95 | nsplits++; | |
| 1149 | } | ||
| 1150 |
2/2✓ Branch 0 taken 214 times.
✓ Branch 1 taken 23 times.
|
237 | else if ( |
| 1151 |
2/2✓ Branch 1 taken 3 times.
✓ Branch 2 taken 20 times.
|
23 | retired && GlobalVisCheckRemovableFullXid( |
| 1152 | m.heap, FullTransactionIdFromU64(dxid))) | ||
| 1153 | { | ||
| 1154 | /* No snapshot can still hold a stale pointer into this chain, so | ||
| 1155 | * it is safe to physically retire it (its own commits are durable | ||
| 1156 | * independent of maint_end, which only persists nlist). */ | ||
| 1157 | 20 | prism_posting_chain_tombstone(&m.storage.base, blk); | |
| 1158 | 20 | nreclaimed++; | |
| 1159 | } | ||
| 1160 | } | ||
| 1161 | |||
| 1162 | 34 | maint_end(index, &m, nsplits > 0); | |
| 1163 | /* | ||
| 1164 | * Keep PRISM_MAINT_LOCK until end of transaction (NoLock here releases the | ||
| 1165 | * reference, not the lock). maint_end queues a relcache invalidation for | ||
| 1166 | * the new leaf count, and that is only delivered at commit -- release the | ||
| 1167 | * lock now and the next maintenance call could take it, still holding a | ||
| 1168 | * cached index base with the stale count, and mint cluster ids that | ||
| 1169 | * collide with the ones just written. | ||
| 1170 | */ | ||
| 1171 | 34 | relation_close(index, NoLock); | |
| 1172 | |||
| 1173 | /* | ||
| 1174 | * Report the reclaim count as well as the split count. A retired chain is | ||
| 1175 | * unreachable from the centroid tree, so nothing that inspects the index | ||
| 1176 | * can see it -- this NOTICE is the only way an operator can tell whether a | ||
| 1177 | * pass freed the chains an earlier one left behind, or whether a reader's | ||
| 1178 | * snapshot is still holding them. | ||
| 1179 | */ | ||
| 1180 |
2/2✓ Branch 1 taken 30 times.
✓ Branch 2 taken 4 times.
|
34 | ereport(NOTICE, |
| 1181 | (errmsg("rebalance: split %d posting list(s), reclaimed %d " | ||
| 1182 | "retired chain(s)", | ||
| 1183 | nsplits, | ||
| 1184 | nreclaimed))); | ||
| 1185 | |||
| 1186 | 34 | PG_RETURN_VOID(); | |
| 1187 | } | ||
| 1188 |