Skip to content

Commit d270b2b

Browse files
committed
[SLOP(claude-opus-4-8-high)] feat(universaldb): postgres leader-resolver driver overhaul
1 parent e9df8e7 commit d270b2b

27 files changed

Lines changed: 1728 additions & 384 deletions

File tree

Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ members = [
6666
"engine/sdks/rust/depot-protocol",
6767
"engine/sdks/rust/test-envoy",
6868
"engine/sdks/rust/ups-protocol",
69+
"engine/sdks/rust/universaldb-commit",
6970
"rivetkit-rust/packages/actor-persist",
7071
"rivetkit-rust/packages/client",
7172
"rivetkit-rust/packages/engine-process",
@@ -638,6 +639,9 @@ members = [
638639
[workspace.dependencies.rivet-ups-protocol]
639640
path = "engine/sdks/rust/ups-protocol"
640641

642+
[workspace.dependencies.rivet-universaldb-commit]
643+
path = "engine/sdks/rust/universaldb-commit"
644+
641645
[profile.dev]
642646
overflow-checks = false
643647
# "line-tables-only" produces just the line-number DWARF needed for stack

engine/packages/universaldb/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ edition.workspace = true
99
[dependencies]
1010
anyhow.workspace = true
1111
async-trait.workspace = true
12+
base64.workspace = true
1213
deadpool-postgres.workspace = true
1314
foundationdb-tuple.workspace = true
1415
futures-util.workspace = true
@@ -18,16 +19,20 @@ rand.workspace = true
1819
rivet-metrics.workspace = true
1920
rivet-postgres-util.workspace = true
2021
rivet-tracing-utils.workspace = true
22+
rivet-universaldb-commit.workspace = true
2123
rocksdb.workspace = true
24+
scc.workspace = true
2225
serde.workspace = true
2326
tempfile.workspace = true
2427
thiserror.workspace = true
2528
tokio-postgres-rustls.workspace = true
2629
tokio-postgres.workspace = true
30+
tokio-util.workspace = true
2731
tokio.workspace = true
2832
tracing.workspace = true
2933
url.workspace = true
3034
uuid.workspace = true
35+
vbare.workspace = true
3136

3237
[dev-dependencies]
3338
rivet-config.workspace = true

engine/packages/universaldb/src/driver/rocksdb/transaction_conflict_tracker.rs renamed to engine/packages/universaldb/src/conflict_tracker.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use tokio::sync::Mutex;
1111
use crate::options::ConflictRangeType;
1212

1313
// Transactions cannot live longer than 5 seconds so we don't need to store transaction conflicts longer than
14-
// that
14+
// that.
1515
const TXN_CONFLICT_TTL: Duration = Duration::from_secs(10);
1616

1717
#[derive(Debug)]
@@ -22,6 +22,15 @@ struct PreviousTransaction {
2222
conflict_ranges: Vec<(Vec<u8>, Vec<u8>, ConflictRangeType)>,
2323
}
2424

25+
/// In-process FoundationDB-style resolver. Holds the last `TXN_CONFLICT_TTL` of committed
26+
/// transactions and rejects a committing transaction if any retained transaction has both an
27+
/// overlapping version window and an overlapping conflict range of a differing type.
28+
///
29+
/// Used by the rocksdb driver (single process) and by the postgres leader-resolver. The two
30+
/// differ only in where the commit version comes from: rocksdb generates it from the in-process
31+
/// `global_version` counter, while the postgres leader assigns it from the durable
32+
/// `udb_version_seq` so it survives leader failover and matches the versionstamp. For that reason
33+
/// `check_and_insert` takes the commit version from the caller instead of generating it.
2534
#[derive(Clone)]
2635
pub struct TransactionConflictTracker {
2736
// NOTE: We use a mutex because we need to lock reads across all active txns. This could be optimized to
@@ -40,18 +49,23 @@ impl TransactionConflictTracker {
4049
}
4150
}
4251

43-
/// Each number returned is unique.
52+
/// Each number returned is unique. Used by the in-process rocksdb driver to assign both start
53+
/// and commit versions. The postgres leader does not use this; it assigns versions from the
54+
/// durable Postgres sequence.
4455
pub fn next_global_version(&self) -> u64 {
4556
self.global_version.fetch_add(1, Ordering::SeqCst)
4657
}
4758

59+
/// Returns `true` on conflict (same polarity as the original rocksdb tracker). The caller
60+
/// supplies `commit_version` (e.g. `nextval('udb_version_seq')` on the postgres leader, or
61+
/// `next_global_version()` on rocksdb) so version assignment stays the caller's responsibility.
4862
pub async fn check_and_insert(
4963
&self,
5064
txn1_start_version: u64,
65+
txn1_commit_version: u64,
5166
txn1_conflict_ranges: Vec<(Vec<u8>, Vec<u8>, ConflictRangeType)>,
5267
) -> bool {
5368
let mut txns = self.txns.lock().await;
54-
let txn1_commit_version = self.next_global_version();
5569

5670
// Prune old entries
5771
txns.retain(|txn| txn.insert_instant.elapsed() < TXN_CONFLICT_TTL);
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
use anyhow::Result;
2+
use rivet_universaldb_commit::{self as proto, versioned};
3+
use vbare::OwnedVersionedData;
4+
5+
use crate::{
6+
options::{ConflictRangeType, MutationType},
7+
tx_ops::Operation,
8+
};
9+
10+
/// Decoded form of a `udb_commit_requests.payload` blob.
11+
///
12+
/// `read_version` is intentionally omitted: it is also denormalized into the `read_version` column,
13+
/// which is what the leader's drain reads, so decoding it here would be dead.
14+
pub struct DecodedCommit {
15+
pub conflict_ranges: Vec<(Vec<u8>, Vec<u8>, ConflictRangeType)>,
16+
pub operations: Vec<Operation>,
17+
}
18+
19+
/// Encode a follower's commit request to the versioned BARE wire format with an embedded version
20+
/// header so a leader running older or newer code can still decode it during a rolling deploy.
21+
pub fn encode_commit_request(
22+
read_version: u64,
23+
conflict_ranges: &[(Vec<u8>, Vec<u8>, ConflictRangeType)],
24+
operations: &[Operation],
25+
) -> Result<Vec<u8>> {
26+
let request = proto::CommitRequest {
27+
read_version,
28+
conflict_ranges: conflict_ranges
29+
.iter()
30+
.map(|(begin, end, kind)| proto::ConflictRange {
31+
begin: begin.clone(),
32+
end: end.clone(),
33+
kind: conflict_range_type_to_proto(*kind),
34+
})
35+
.collect(),
36+
operations: operations.iter().map(operation_to_proto).collect(),
37+
};
38+
39+
versioned::CommitRequest::wrap_latest(request)
40+
.serialize_with_embedded_version(proto::PROTOCOL_VERSION)
41+
}
42+
43+
/// Decode a `udb_commit_requests.payload` blob produced by [`encode_commit_request`].
44+
pub fn decode_commit_request(payload: &[u8]) -> Result<DecodedCommit> {
45+
let request = versioned::CommitRequest::deserialize_with_embedded_version(payload)?;
46+
47+
let conflict_ranges = request
48+
.conflict_ranges
49+
.into_iter()
50+
.map(|range| {
51+
(
52+
range.begin,
53+
range.end,
54+
conflict_range_type_from_proto(range.kind),
55+
)
56+
})
57+
.collect();
58+
59+
let operations = request
60+
.operations
61+
.into_iter()
62+
.map(operation_from_proto)
63+
.collect();
64+
65+
Ok(DecodedCommit {
66+
conflict_ranges,
67+
operations,
68+
})
69+
}
70+
71+
fn conflict_range_type_to_proto(kind: ConflictRangeType) -> proto::ConflictRangeType {
72+
match kind {
73+
ConflictRangeType::Read => proto::ConflictRangeType::Read,
74+
ConflictRangeType::Write => proto::ConflictRangeType::Write,
75+
}
76+
}
77+
78+
fn conflict_range_type_from_proto(kind: proto::ConflictRangeType) -> ConflictRangeType {
79+
match kind {
80+
proto::ConflictRangeType::Read => ConflictRangeType::Read,
81+
proto::ConflictRangeType::Write => ConflictRangeType::Write,
82+
}
83+
}
84+
85+
fn operation_to_proto(op: &Operation) -> proto::Operation {
86+
match op {
87+
Operation::SetValue { key, value } => proto::Operation::SetValue(proto::SetValue {
88+
key: key.clone(),
89+
value: value.clone(),
90+
}),
91+
Operation::Clear { key } => proto::Operation::Clear(proto::Clear { key: key.clone() }),
92+
Operation::ClearRange { begin, end } => proto::Operation::ClearRange(proto::ClearRange {
93+
begin: begin.clone(),
94+
end: end.clone(),
95+
}),
96+
Operation::AtomicOp {
97+
key,
98+
param,
99+
op_type,
100+
} => proto::Operation::AtomicOp(proto::AtomicOp {
101+
key: key.clone(),
102+
param: param.clone(),
103+
op_type: mutation_type_to_proto(*op_type),
104+
}),
105+
}
106+
}
107+
108+
fn operation_from_proto(op: proto::Operation) -> Operation {
109+
match op {
110+
proto::Operation::SetValue(proto::SetValue { key, value }) => {
111+
Operation::SetValue { key, value }
112+
}
113+
proto::Operation::Clear(proto::Clear { key }) => Operation::Clear { key },
114+
proto::Operation::ClearRange(proto::ClearRange { begin, end }) => {
115+
Operation::ClearRange { begin, end }
116+
}
117+
proto::Operation::AtomicOp(proto::AtomicOp {
118+
key,
119+
param,
120+
op_type,
121+
}) => Operation::AtomicOp {
122+
key,
123+
param,
124+
op_type: mutation_type_from_proto(op_type),
125+
},
126+
}
127+
}
128+
129+
fn mutation_type_to_proto(op_type: MutationType) -> proto::MutationType {
130+
match op_type {
131+
MutationType::Add => proto::MutationType::Add,
132+
MutationType::And => proto::MutationType::And,
133+
MutationType::BitAnd => proto::MutationType::BitAnd,
134+
MutationType::Or => proto::MutationType::Or,
135+
MutationType::BitOr => proto::MutationType::BitOr,
136+
MutationType::Xor => proto::MutationType::Xor,
137+
MutationType::BitXor => proto::MutationType::BitXor,
138+
MutationType::AppendIfFits => proto::MutationType::AppendIfFits,
139+
MutationType::Max => proto::MutationType::Max,
140+
MutationType::Min => proto::MutationType::Min,
141+
MutationType::SetVersionstampedKey => proto::MutationType::SetVersionstampedKey,
142+
MutationType::SetVersionstampedValue => proto::MutationType::SetVersionstampedValue,
143+
MutationType::ByteMin => proto::MutationType::ByteMin,
144+
MutationType::ByteMax => proto::MutationType::ByteMax,
145+
MutationType::CompareAndClear => proto::MutationType::CompareAndClear,
146+
}
147+
}
148+
149+
fn mutation_type_from_proto(op_type: proto::MutationType) -> MutationType {
150+
match op_type {
151+
proto::MutationType::Add => MutationType::Add,
152+
proto::MutationType::And => MutationType::And,
153+
proto::MutationType::BitAnd => MutationType::BitAnd,
154+
proto::MutationType::Or => MutationType::Or,
155+
proto::MutationType::BitOr => MutationType::BitOr,
156+
proto::MutationType::Xor => MutationType::Xor,
157+
proto::MutationType::BitXor => MutationType::BitXor,
158+
proto::MutationType::AppendIfFits => MutationType::AppendIfFits,
159+
proto::MutationType::Max => MutationType::Max,
160+
proto::MutationType::Min => MutationType::Min,
161+
proto::MutationType::SetVersionstampedKey => MutationType::SetVersionstampedKey,
162+
proto::MutationType::SetVersionstampedValue => MutationType::SetVersionstampedValue,
163+
proto::MutationType::ByteMin => MutationType::ByteMin,
164+
proto::MutationType::ByteMax => MutationType::ByteMax,
165+
proto::MutationType::CompareAndClear => MutationType::CompareAndClear,
166+
}
167+
}

0 commit comments

Comments
 (0)