|
| 1 | +// Copyright 2026 The Cockroach Authors. |
| 2 | +// |
| 3 | +// Use of this software is governed by the CockroachDB Software License |
| 4 | +// included in the /LICENSE file. |
| 5 | + |
| 6 | +/* |
| 7 | +Package lease manages localized, consistently cached versions of SQL descriptors |
| 8 | +(such as tables, views, sequences, databases, and schemas) on each CockroachDB node. |
| 9 | +It serves as the critical bridge between Schema Changes (DDL) and query |
| 10 | +execution (DML) by tracking exactly which descriptor versions are currently |
| 11 | +in use across the cluster. |
| 12 | +
|
| 13 | +By providing highly available, temporally consistent schema definitions, the |
| 14 | +lease manager ensures that queries execute efficiently. Without this layer, |
| 15 | +every transaction would have to fetch metadata directly from the |
| 16 | +system.descriptor table, creating a massive bottleneck and injecting |
| 17 | +significant latency into every query. |
| 18 | +
|
| 19 | +# The Performance vs. Consistency Problem |
| 20 | +
|
| 21 | +In a distributed SQL database, descriptors dictate the shape and layout of data. |
| 22 | +If every transaction had to query the cluster's central system.descriptor table |
| 23 | +for schema details before executing, the system table would become a massive |
| 24 | +bottleneck, crippling cluster throughput and injecting significant latency. |
| 25 | +
|
| 26 | +Conversely, relying on a naive local cache is dangerous. In a distributed |
| 27 | +system, schema changes can occur concurrently with running queries. If Node A |
| 28 | +caches a table schema and Node B drops a column, Node A might write a tuple that |
| 29 | +violates the new schema, causing irrecoverable data corruption. |
| 30 | +
|
| 31 | +# The Lease Solution |
| 32 | +
|
| 33 | +A lease acts as a localized leasehold on a specific, immutable version of a |
| 34 | +descriptor. It grants a node the explicit right to use that version for the |
| 35 | +duration of a transaction. More importantly, publishing this lease to the |
| 36 | +system.lease table makes the node's schema dependency visible to the rest of |
| 37 | +the cluster. This allows schema change orchestrators (DDL operations) to safely |
| 38 | +pause and wait for nodes to finish using old schemas before finalizing a |
| 39 | +schema change. |
| 40 | +
|
| 41 | +# Core Components |
| 42 | +
|
| 43 | + - Manager: The primary entry point and orchestration struct. It acts as the |
| 44 | + bridge between the node's local caches, the system tables, and range feeds. |
| 45 | + - descriptorState: The top-level cache entity for a single descriptor ID. It |
| 46 | + holds the active descriptorSet along with metadata like the maximum version |
| 47 | + seen and an offline flag. |
| 48 | + - descriptorSet: Maintains an ordered, chronologically sound slice of |
| 49 | + descriptorVersionState objects. |
| 50 | + - descriptorVersionState: Represents an instantiated, leased copy of a |
| 51 | + descriptor version. It embeds the catalog.Descriptor and includes a |
| 52 | + thread-safe reference count (refcount). |
| 53 | + - nameCache: A specialized cache mapping human-readable identifiers— |
| 54 | + (ParentID, SchemaID, ObjectName)—to canonical descriptor IDs. |
| 55 | +
|
| 56 | +# Key Architectural Invariants |
| 57 | +
|
| 58 | +# The Validity Interval ([ModificationTime, ExpirationTime)) |
| 59 | +
|
| 60 | +Every leased descriptor defines a Validity Interval spanning from its creation |
| 61 | +to its eventual expiration. |
| 62 | + - ModificationTime: The exact timestamp when this specific version of the |
| 63 | + descriptor was committed to the KV layer. |
| 64 | + - ExpirationTime: The timestamp when this lease becomes strictly invalid for |
| 65 | + new transactions. |
| 66 | +
|
| 67 | +For a transaction to successfully use a specific lease version, its |
| 68 | +ReadTimestamp must fall strictly within this interval: |
| 69 | +ModificationTime <= txn.ReadTimestamp < ExpirationTime. |
| 70 | +
|
| 71 | +This model guarantees perfectly consistent MVCC schema caching, including for |
| 72 | +historical queries (AS OF SYSTEM TIME). |
| 73 | +
|
| 74 | +When a node acquires a new version (N+1) of a descriptor, it immediately |
| 75 | +"closes" the validity interval of the previous version (N) by setting its |
| 76 | +ExpirationTime to a fixed point in the near future. This allows existing |
| 77 | +transactions using version N to complete their work while ensuring that all |
| 78 | +new transactions are directed to version N+1. |
| 79 | +
|
| 80 | +# The Two-Version Invariant and DDL Synchronization |
| 81 | +
|
| 82 | +Schema changes happen in gradual state transitions. To prevent data corruption, |
| 83 | +at most two adjacent versions of a descriptor can be actively leased cluster-wide |
| 84 | +at any given time (e.g., version N and N+1). This guarantees that nodes across |
| 85 | +the cluster are never more than one schema transition state apart. The lease |
| 86 | +manager enforces this locally and will panic if it detects a violation, as it |
| 87 | +implies a breakdown in cluster consistency. |
| 88 | +
|
| 89 | +The lease package exposes several critical blocking APIs used by schema changes |
| 90 | +to ensure appropriate schema versions are leased across the cluster. These functions |
| 91 | +rely on polling the system.lease table to verify cluster-wide state: |
| 92 | + - WaitForOneVersion: Blocks until all unexpired leases on the previous version |
| 93 | + of a descriptor have drained. This is used to maintain the two-version |
| 94 | + invariant, ensuring that no nodes in the cluster are more than one version |
| 95 | + apart. |
| 96 | + - WaitForNewVersion: Blocks until every alive node holding a lease on an |
| 97 | + older version of a descriptor has also acquired a lease on the latest |
| 98 | + version. This ensures that the newest version is visible to all active |
| 99 | + leaseholders. |
| 100 | + - WaitForInitialVersion: Blocks until a lease for the initial version (version 1) |
| 101 | + of a newly created descriptor has been acquired by all nodes that currently |
| 102 | + hold a lease on the parent schema. This ensures that any node capable of |
| 103 | + resolving the name via its schema lease will also have the descriptor |
| 104 | + cached, preventing inconsistent query planning or "table not found" errors. |
| 105 | + This wait applies specifically to tables and types and is controlled by the |
| 106 | + sql.catalog.descriptor_wait_for_initial_version.enabled cluster setting. |
| 107 | + - WaitForNoVersion: Blocks until no unexpired leases exist for any version |
| 108 | + of a descriptor. This is used during descriptor deletion (e.g., DROP TABLE) |
| 109 | + to ensure all nodes have stopped using the object. |
| 110 | + - Liveness Integration: To avoid infinite hangs if a node crashes while |
| 111 | + holding a lease, these wait loops rely on the sqlliveness layer. If |
| 112 | + system.lease shows a lease for a node, but that node's liveness record |
| 113 | + has expired, the wait loop safely ignores that lease. |
| 114 | +
|
| 115 | +# Session-Based Leases |
| 116 | +
|
| 117 | +The lease subsystem implements session based leasing, where leases are tethered |
| 118 | +to the node's sqlliveness session. As long as the SQL node is healthy |
| 119 | +(heartbeating its liveness record), the latest version of a lease has an |
| 120 | +effectively infinite expiration. |
| 121 | +
|
| 122 | +However, this expiration is only "infinite" while it remains the newest version |
| 123 | +held by the node. When a more recent version is acquired, the lease manager |
| 124 | +explicitly "closes" the older version's validity interval by setting a concrete |
| 125 | +ExpirationTime in the near future, allowing existing transactions to drain. If a |
| 126 | +node dies, its session expires, automatically and safely invalidating all its |
| 127 | +held leases across the cluster. |
| 128 | +
|
| 129 | +# Cache Coherence and Range Feeds |
| 130 | +
|
| 131 | +To maintain localized caches without stale reads, the lease manager actively |
| 132 | +watches the system.descriptor table using a RangeFeed. |
| 133 | +
|
| 134 | + - Detection of New Versions: The RangeFeed provides a real-time stream of |
| 135 | + updates to the system.descriptor table. When a schema change is published, |
| 136 | + the RangeFeed pushes the new descriptor version to all nodes. The lease |
| 137 | + manager observes this update and triggers a background task to refresh the |
| 138 | + local cache. This involves marking any existing leases for the old version |
| 139 | + for expiration and incrementing the local leaseGeneration count to |
| 140 | + invalidate metadata caches (like optimizer memos). |
| 141 | +
|
| 142 | + - Proactive Initial Acquisition: When a new relation or type is created |
| 143 | + within a schema that is already leased locally, the lease manager can |
| 144 | + proactively acquire a lease on the new object immediately upon seeing the |
| 145 | + RangeFeed event. This behavior, controlled by cluster settings, ensures the |
| 146 | + cluster converges quickly on new schema objects and maintains consistent |
| 147 | + optimizer state across nodes. |
| 148 | +
|
| 149 | + - Range Feed Recovery: A background watchdog monitors the feed. If the feed |
| 150 | + stops receiving updates, the node marks the cache as potentially stale. |
| 151 | + Upon recovery, a Full Refresh is triggered to proactively re-validate all |
| 152 | + currently leased descriptors. |
| 153 | +
|
| 154 | +# Locked Leasing |
| 155 | +
|
| 156 | +To maintain strong transactional consistency, the manager maintains a |
| 157 | +closeTimestamp reflecting the high-water mark of ingested updates from the |
| 158 | +RangeFeed. When a transaction begins query planning, it can obtain a "locked" |
| 159 | +timestamp. This ensures that all subsequent descriptor acquisitions for that |
| 160 | +transaction are pinned to the same point-in-time snapshot, preventing |
| 161 | +inconsistencies that could occur if some tables were updated in the cache while |
| 162 | +others were still being processed. |
| 163 | +
|
| 164 | +# Lease Generation |
| 165 | +
|
| 166 | +The lease manager maintains a monotonically increasing counter known as the |
| 167 | +lease generation. It serves as a lightweight mechanism for other subsystems—such |
| 168 | +as the SQL optimizer's memo cache or query plan caches—to quickly determine if |
| 169 | +their cached metadata might be stale. By checking if the lease generation has |
| 170 | +changed since a cache entry was created, these subsystems can safely invalidate |
| 171 | +their caches without having to inspect individual descriptor versions or perform |
| 172 | +slow lookups. |
| 173 | +
|
| 174 | +The generation counter is explicitly bumped (incremented) in the following |
| 175 | +scenarios to broadcast that new catalog data is available: |
| 176 | +
|
| 177 | + - RangeFeed Updates: When a new descriptor version is published to the |
| 178 | + system.descriptor table and the lease manager processes the update via its |
| 179 | + RangeFeed (e.g., to purge older versions or proactively acquire an initial |
| 180 | + version). |
| 181 | + - Descriptor Deletion: When a descriptor is marked as dropped or offline, and |
| 182 | + the manager purges its old versions. |
| 183 | + - Manual Refresh: At the completion of a full manual refresh of the local |
| 184 | + descriptor cache (typically triggered upon RangeFeed recovery). |
| 185 | + - Synchronous System Updates: During the local acquisition of specific |
| 186 | + system-level descriptors (like users or privileges), the generation is |
| 187 | + bumped synchronously. This forces dependent queries to replan immediately, |
| 188 | + rather than waiting for the asynchronous RangeFeed update to arrive. |
| 189 | +
|
| 190 | +# Lease Observers |
| 191 | +
|
| 192 | +Subsystems can register for granular notifications of descriptor updates using |
| 193 | +the Observer interface via RegisterLeaseObserver. While the global lease |
| 194 | +generation counter signals a broad catalog change, observers receive targeted |
| 195 | +asynchronous notifications (OnNewVersion) for specific descriptor IDs as soon |
| 196 | +as they are processed by the local RangeFeed. |
| 197 | +
|
| 198 | +This proactive notification mechanism is critical for components like |
| 199 | +Changefeeds (schemaFeed) to track schema transitions in real-time. By reacting |
| 200 | +immediately to new versions, these observers can accurately advance their |
| 201 | +internal frontiers or trigger background tasks to refresh specialized caches, |
| 202 | +ensuring high-fidelity tracking of schema state across the cluster. |
| 203 | +
|
| 204 | +# Lease Acquisition and Cache Misses |
| 205 | +
|
| 206 | +When a transaction needs a descriptor that is not currently valid in the local |
| 207 | +cache, the manager performs a reactive acquisition: |
| 208 | +
|
| 209 | + - KV Acquisition: The manager reads the descriptor's current state from the |
| 210 | + KV store and writes a corresponding lease entry to the system.lease table. |
| 211 | + - Singleflight Protection: To prevent a "thundering herd" effect when a |
| 212 | + popular table's lease expires, the manager uses a singleflight mechanism to |
| 213 | + ensure that only one goroutine performs the KV acquisition for a given |
| 214 | + descriptor at a time. |
| 215 | + - Memory Accounting: Every acquired lease is registered against a memory |
| 216 | + monitor to prevent the cache from growing unbounded and causing |
| 217 | + out-of-memory errors. |
| 218 | +
|
| 219 | +# Bulk Acquisition |
| 220 | +
|
| 221 | +To improve throughput for complex queries requiring many descriptors (e.g., |
| 222 | +information_schema queries), the manager supports Bulk Acquisition (EnsureBatch). |
| 223 | +This allows multiple leases to be acquired in a single batched KV transaction, |
| 224 | +amortizing the network overhead. |
| 225 | +
|
| 226 | +# Descriptor Name Resolution |
| 227 | +
|
| 228 | +Every SQL statement parsing step needs to resolve human-readable strings like |
| 229 | +public.users into a canonical descriptor ID before formulating a query plan. |
| 230 | +Performing a KV lookup for every identifier in a complex query would introduce |
| 231 | +enormous latency. |
| 232 | +
|
| 233 | +To mitigate this, the lease manager maintains a specialized nameCache mapping |
| 234 | +(ParentID, SchemaID, ObjectName) to descpb.ID. Because names can be reassigned |
| 235 | +(e.g., via RENAME TABLE), the cache carefully tracks modification timestamps |
| 236 | +alongside lease acquisitions and releases to ensure name resolutions accurately |
| 237 | +reflect the point-in-time state of the database, enabling sub-millisecond query |
| 238 | +planning. |
| 239 | +
|
| 240 | +# Handling Dropped and Offline Descriptors |
| 241 | +
|
| 242 | +When a table or other descriptor is dropped, a final "tombstone" version is |
| 243 | +published to the system.descriptor table. |
| 244 | +
|
| 245 | + - The takenOffline Flag: The RangeFeed observes this tombstone version and |
| 246 | + flags the internal descriptorState as takenOffline. |
| 247 | + - Immediate Eviction: Once a descriptor is offline, as soon as its active |
| 248 | + refcount hits zero, the manager immediately evicts it from memory and |
| 249 | + storage. Any subsequent Release() of an offline descriptor will prevent new |
| 250 | + leases from being acquired, safely blocking new queries from accessing the |
| 251 | + deleted object. |
| 252 | +
|
| 253 | +# Lease Cleanup |
| 254 | +
|
| 255 | +Stale leases are not immediately deleted from the system.lease table. Instead, |
| 256 | +when a lease's validity interval is closed (due to a new version arrival), it is |
| 257 | +added to an internal expiration queue. A background task periodically sweeps |
| 258 | +this queue and removes physical rows from the KV store once their expiration |
| 259 | +timestamp has passed and their in-memory reference count has dropped to zero. |
| 260 | +This ensures the system.lease table remains lean while allowing in-flight |
| 261 | +transactions enough time to finish. |
| 262 | +
|
| 263 | +# Multi-Region Considerations |
| 264 | +
|
| 265 | +In multi-region clusters, the lease manager uses the regionliveness |
| 266 | +subsystem to perform region-aware counts of active leases. This allows the |
| 267 | +system to efficiently track cluster-wide schema dependencies even when some |
| 268 | +regions are experiencing network partitions or latency spikes, ensuring that |
| 269 | +schema changes can proceed safely without waiting indefinitely for unreachable |
| 270 | +nodes. |
| 271 | +
|
| 272 | +# Node Lifecycle |
| 273 | +
|
| 274 | + - Startup: The node initializes the manager, cleans up orphaned leases from |
| 275 | + previous incarnations, and establishes the RangeFeed to warm caches. |
| 276 | + - Draining: During graceful shutdown, the node enters a draining state where |
| 277 | + it refuses new leases and waits for active reference counts to hit zero |
| 278 | + before deleting its leases from the KV store. |
| 279 | +*/ |
| 280 | +package lease |
0 commit comments