Skip to content

Commit 244e7c9

Browse files
committed
Harden transactional delta capture
1 parent ec74d2c commit 244e7c9

9 files changed

Lines changed: 459 additions & 95 deletions

File tree

src/core/refresh_locks.cpp

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,41 @@ namespace duckdb {
55
std::mutex RefreshLocks::map_mutex_;
66
std::unordered_map<string, unique_ptr<std::mutex>> RefreshLocks::view_mutexes_;
77
std::unordered_map<string, unique_ptr<std::mutex>> RefreshLocks::delta_mutexes_;
8-
std::unordered_map<string, unique_ptr<std::mutex>> RefreshLocks::delta_catalog_mutexes_;
8+
std::unordered_map<const Catalog *, unique_ptr<DeltaCatalogPhaseGate>> RefreshLocks::delta_catalog_gates_;
9+
10+
void DeltaCatalogPhaseGate::EnterWrite() {
11+
std::unique_lock<mutex> guard(lock);
12+
// Once a refresh is waiting, stop admitting new writers so the finite set of
13+
// active transactions can drain. Writers otherwise run concurrently.
14+
condition.wait(guard, [&]() { return active_refreshes == 0 && waiting_refreshes == 0; });
15+
active_writers++;
16+
}
17+
18+
void DeltaCatalogPhaseGate::ExitWrite() {
19+
lock_guard<mutex> guard(lock);
20+
D_ASSERT(active_writers > 0);
21+
active_writers--;
22+
if (active_writers == 0) {
23+
condition.notify_all();
24+
}
25+
}
26+
27+
void DeltaCatalogPhaseGate::EnterRefresh() {
28+
std::unique_lock<mutex> guard(lock);
29+
waiting_refreshes++;
30+
condition.wait(guard, [&]() { return active_writers == 0; });
31+
waiting_refreshes--;
32+
active_refreshes++;
33+
}
34+
35+
void DeltaCatalogPhaseGate::ExitRefresh() {
36+
lock_guard<mutex> guard(lock);
37+
D_ASSERT(active_refreshes > 0);
38+
active_refreshes--;
39+
if (active_refreshes == 0) {
40+
condition.notify_all();
41+
}
42+
}
943

1044
std::mutex &RefreshLocks::GetViewMutex(const string &view_name) {
1145
std::lock_guard<std::mutex> guard(map_mutex_);
@@ -25,11 +59,11 @@ std::mutex &RefreshLocks::GetDeltaMutex(const string &delta_table_name) {
2559
return *entry;
2660
}
2761

28-
std::mutex &RefreshLocks::GetDeltaCatalogMutex(const string &catalog_name) {
62+
DeltaCatalogPhaseGate &RefreshLocks::GetDeltaCatalogGate(Catalog &catalog) {
2963
std::lock_guard<std::mutex> guard(map_mutex_);
30-
auto &entry = delta_catalog_mutexes_[catalog_name];
64+
auto &entry = delta_catalog_gates_[&catalog];
3165
if (!entry) {
32-
entry = duckdb::unique_ptr<std::mutex>(new std::mutex());
66+
entry = make_uniq<DeltaCatalogPhaseGate>();
3367
}
3468
return *entry;
3569
}
@@ -54,12 +88,20 @@ void RefreshLocks::UnlockDelta(const string &delta_table_name) {
5488
GetDeltaMutex(delta_table_name).unlock();
5589
}
5690

57-
void RefreshLocks::LockDeltaCatalog(const string &catalog_name) {
58-
GetDeltaCatalogMutex(catalog_name).lock();
91+
void RefreshLocks::EnterDeltaWrite(Catalog &catalog) {
92+
GetDeltaCatalogGate(catalog).EnterWrite();
93+
}
94+
95+
void RefreshLocks::ExitDeltaWrite(Catalog &catalog) {
96+
GetDeltaCatalogGate(catalog).ExitWrite();
97+
}
98+
99+
void RefreshLocks::EnterDeltaRefresh(Catalog &catalog) {
100+
GetDeltaCatalogGate(catalog).EnterRefresh();
59101
}
60102

61-
void RefreshLocks::UnlockDeltaCatalog(const string &catalog_name) {
62-
GetDeltaCatalogMutex(catalog_name).unlock();
103+
void RefreshLocks::ExitDeltaRefresh(Catalog &catalog) {
104+
GetDeltaCatalogGate(catalog).ExitRefresh();
63105
}
64106

65107
} // namespace duckdb

src/core/refresh_metadata.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,28 @@ RefreshMetadata::SourceLocation RefreshMetadata::GetSourceLocation(const string
122122
return loc;
123123
}
124124

125+
vector<RefreshMetadata::DeltaSource> RefreshMetadata::GetDeltaSources(const string &view_name,
126+
const string &fallback_catalog,
127+
const string &fallback_schema) {
128+
auto result = con.Query("SELECT table_name, catalog_type, source_catalog, source_schema FROM " +
129+
string(openivm::DELTA_TABLES_TABLE) + " WHERE view_name = '" +
130+
SqlUtils::EscapeValue(view_name) + "'");
131+
vector<DeltaSource> sources;
132+
if (result->HasError()) {
133+
return sources;
134+
}
135+
for (idx_t row = 0; row < result->RowCount(); row++) {
136+
DeltaSource source;
137+
source.table_name = result->GetValue(0, row).ToString();
138+
source.catalog_type = result->GetValue(1, row).IsNull() ? "duckdb" : result->GetValue(1, row).ToString();
139+
source.catalog_name =
140+
result->GetValue(2, row).IsNull() ? fallback_catalog : result->GetValue(2, row).ToString();
141+
source.schema_name = result->GetValue(3, row).IsNull() ? fallback_schema : result->GetValue(3, row).ToString();
142+
sources.push_back(std::move(source));
143+
}
144+
return sources;
145+
}
146+
125147
string RefreshMetadata::ResolveDeltaQualifiedName(const string &view_name, const string &delta_table_name,
126148
const string &fallback_catalog, const string &fallback_schema) {
127149
auto loc = GetSourceLocation(view_name, delta_table_name, fallback_catalog, fallback_schema);
@@ -259,6 +281,12 @@ vector<string> RefreshMetadata::GetDownstreamViews(const string &view_name) {
259281
return result; // topological order: downstream parents before fan-in children
260282
}
261283

284+
bool RefreshMetadata::HasDownstreamViews(const string &view_name) {
285+
auto result = con.Query("SELECT 1 FROM " + string(openivm::DELTA_TABLES_TABLE) + " WHERE table_name = '" +
286+
SqlUtils::EscapeValue(SqlUtils::DeltaName(view_name)) + "' LIMIT 1");
287+
return !result->HasError() && result->RowCount() > 0;
288+
}
289+
262290
vector<string> RefreshMetadata::GetGroupColumns(const string &view_name) {
263291
auto result = con.Query("SELECT group_columns FROM " + string(openivm::VIEWS_TABLE) + " WHERE view_name = '" +
264292
SqlUtils::EscapeValue(view_name) + "'");

src/include/core/refresh_locks.hpp

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include "duckdb.hpp"
55

6+
#include <condition_variable>
67
#include <mutex>
78
#include <unordered_map>
89

@@ -13,9 +14,26 @@ namespace duckdb {
1314
// Per-view mutex: prevents two concurrent refreshes of the same MV (which would
1415
// cause write-write conflicts on the MV table).
1516
//
16-
// Per-delta-table mutex: serializes the refresh's "read deltas + set last_update"
17-
// critical section with the insert rule's delta row writes. This closes the window
18-
// where concurrent DML deltas could be permanently skipped.
17+
// Per-delta-table mutex: serializes refreshes that consume the same delta table.
18+
//
19+
// Per-catalog phase gate: allows concurrent DML writers and concurrent refreshes, but
20+
// prevents those two phases from overlapping. This closes the window where refresh
21+
// could advance a watermark past a delta row that commits afterward.
22+
class DeltaCatalogPhaseGate {
23+
public:
24+
void EnterWrite();
25+
void ExitWrite();
26+
void EnterRefresh();
27+
void ExitRefresh();
28+
29+
private:
30+
mutex lock;
31+
std::condition_variable condition;
32+
idx_t active_writers = 0;
33+
idx_t active_refreshes = 0;
34+
idx_t waiting_refreshes = 0;
35+
};
36+
1937
class RefreshLocks {
2038
public:
2139
// --- View-level locks (prevent concurrent refresh of same MV) ---
@@ -29,47 +47,61 @@ class RefreshLocks {
2947

3048
static void UnlockView(const string &view_name);
3149

32-
// --- Delta-table-level locks (serialize delta reads/writes) ---
50+
// --- Delta-table-level locks (serialize overlapping refreshes) ---
3351

34-
// Blocking lock — held briefly by both refresh (read + timestamp update)
35-
// and insert rule (delta row write).
52+
// Blocking lock held by refresh while consuming and checkpointing one delta table.
3653
static void LockDelta(const string &delta_table_name);
3754

3855
static void UnlockDelta(const string &delta_table_name);
3956

40-
// Serializes refresh with transactions that write native delta tables in the same catalog.
41-
// The writer holds this lock until transaction commit/rollback so a refresh cannot advance
42-
// its timestamp cursor past an uncommitted delta row.
43-
static void LockDeltaCatalog(const string &catalog_name);
44-
45-
static void UnlockDeltaCatalog(const string &catalog_name);
57+
static void EnterDeltaWrite(Catalog &catalog);
58+
static void ExitDeltaWrite(Catalog &catalog);
59+
static void EnterDeltaRefresh(Catalog &catalog);
60+
static void ExitDeltaRefresh(Catalog &catalog);
4661

4762
private:
4863
static std::mutex &GetViewMutex(const string &view_name);
4964
static std::mutex &GetDeltaMutex(const string &delta_table_name);
50-
static std::mutex &GetDeltaCatalogMutex(const string &catalog_name);
65+
static DeltaCatalogPhaseGate &GetDeltaCatalogGate(Catalog &catalog);
5166

5267
static std::mutex map_mutex_;
5368
static std::unordered_map<string, unique_ptr<std::mutex>> view_mutexes_;
5469
static std::unordered_map<string, unique_ptr<std::mutex>> delta_mutexes_;
55-
static std::unordered_map<string, unique_ptr<std::mutex>> delta_catalog_mutexes_;
70+
static std::unordered_map<const Catalog *, unique_ptr<DeltaCatalogPhaseGate>> delta_catalog_gates_;
5671
};
5772

58-
// RAII guard for catalog-level delta transaction locks.
59-
class DeltaCatalogLockGuard {
60-
string name_;
73+
// Write guards can be retained by transaction state and released from the commit thread;
74+
// unlike std::mutex ownership, phase-gate membership is not tied to one OS thread.
75+
class DeltaCatalogWriteGuard {
76+
Catalog *catalog;
77+
78+
public:
79+
explicit DeltaCatalogWriteGuard(Catalog &catalog_p) : catalog(&catalog_p) {
80+
RefreshLocks::EnterDeltaWrite(*catalog);
81+
}
82+
~DeltaCatalogWriteGuard() {
83+
RefreshLocks::ExitDeltaWrite(*catalog);
84+
}
85+
DeltaCatalogWriteGuard(const DeltaCatalogWriteGuard &) = delete;
86+
DeltaCatalogWriteGuard &operator=(const DeltaCatalogWriteGuard &) = delete;
87+
DeltaCatalogWriteGuard(DeltaCatalogWriteGuard &&) = delete;
88+
DeltaCatalogWriteGuard &operator=(DeltaCatalogWriteGuard &&) = delete;
89+
};
90+
91+
class DeltaCatalogRefreshGuard {
92+
Catalog *catalog;
6193

6294
public:
63-
explicit DeltaCatalogLockGuard(const string &catalog_name) : name_(catalog_name) {
64-
RefreshLocks::LockDeltaCatalog(name_);
95+
explicit DeltaCatalogRefreshGuard(Catalog &catalog_p) : catalog(&catalog_p) {
96+
RefreshLocks::EnterDeltaRefresh(*catalog);
6597
}
66-
~DeltaCatalogLockGuard() {
67-
RefreshLocks::UnlockDeltaCatalog(name_);
98+
~DeltaCatalogRefreshGuard() {
99+
RefreshLocks::ExitDeltaRefresh(*catalog);
68100
}
69-
DeltaCatalogLockGuard(const DeltaCatalogLockGuard &) = delete;
70-
DeltaCatalogLockGuard &operator=(const DeltaCatalogLockGuard &) = delete;
71-
DeltaCatalogLockGuard(DeltaCatalogLockGuard &&) = delete;
72-
DeltaCatalogLockGuard &operator=(DeltaCatalogLockGuard &&) = delete;
101+
DeltaCatalogRefreshGuard(const DeltaCatalogRefreshGuard &) = delete;
102+
DeltaCatalogRefreshGuard &operator=(const DeltaCatalogRefreshGuard &) = delete;
103+
DeltaCatalogRefreshGuard(DeltaCatalogRefreshGuard &&) = delete;
104+
DeltaCatalogRefreshGuard &operator=(DeltaCatalogRefreshGuard &&) = delete;
73105
};
74106

75107
// RAII guard for delta-table locks. Automatically unlocks on scope exit (including exceptions).

src/include/core/refresh_metadata.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,17 @@ class RefreshMetadata {
5454
string schema_name;
5555
string table_name;
5656
};
57+
struct DeltaSource {
58+
string table_name;
59+
string catalog_type;
60+
string catalog_name;
61+
string schema_name;
62+
};
5763

5864
SourceLocation GetSourceLocation(const string &view_name, const string &table_name,
5965
const string &fallback_catalog = "", const string &fallback_schema = "");
66+
vector<DeltaSource> GetDeltaSources(const string &view_name, const string &fallback_catalog = "",
67+
const string &fallback_schema = "");
6068
string ResolveDeltaQualifiedName(const string &view_name, const string &delta_table_name,
6169
const string &fallback_catalog = "", const string &fallback_schema = "");
6270

@@ -79,6 +87,7 @@ class RefreshMetadata {
7987
// Get all downstream MV dependents in topological order (closest first).
8088
// For table→mv1→mv2→mv3, GetDownstreamViews("mv1") returns ["mv2", "mv3"].
8189
vector<string> GetDownstreamViews(const string &view_name);
90+
bool HasDownstreamViews(const string &view_name);
8291

8392
// Get refresh_interval in seconds for a view. Returns -1 if not set (manual only).
8493
int64_t GetRefreshInterval(const string &view_name);

0 commit comments

Comments
 (0)