Skip to content

Commit dcce97a

Browse files
catalog/lease: add lease manager documentation (#167434)
catalog/lease: add lease manager documentation
2 parents d02da49 + 89bfa13 commit dcce97a

2 files changed

Lines changed: 281 additions & 0 deletions

File tree

pkg/sql/catalog/lease/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ go_library(
77
"descriptor_set.go",
88
"descriptor_state.go",
99
"descriptor_version_state.go",
10+
"doc.go",
1011
"kv_writer.go",
1112
"lease.go",
1213
"lease_test_utils.go",

pkg/sql/catalog/lease/doc.go

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
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

Comments
 (0)