Skip to content

Commit d4be14b

Browse files
committed
feat(moonzero): etcd v3 KV protobuf messages — the base of a real etcd client.
The registry models etcd v3 semantics but talks to no live etcd; the first step to a real network client is the etcdserverpb KV wire messages. KeyValue / RangeRequest / RangeResponse / PutRequest now encode and decode over moonrpc's protobuf runtime with the field numbers etcd's rpc.proto fixes, so they are byte-compatible with a real etcd server. Verified against reference vectors CPython's protobuf produced for the same messages (mutation-checked on a field number) and round-tripped on all four backends; the dependency on moonrpc is bumped to 0.7.0 for its PbWriter/PbReader. Signed-off-by: 林晨 (Leo Cheng) <chengkelfan@qq.com>
1 parent 57524f9 commit d4be14b

3 files changed

Lines changed: 302 additions & 1 deletion

File tree

etcd.mbt

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// The etcd v3 KV RPC message layer (`etcdserverpb`), the first foundation of a real
2+
// etcd network client for service discovery (← go-zero's `discov`, which talks to a
3+
// live etcd over gRPC). The messages are self-built on moonrpc's protobuf runtime —
4+
// the same `PbWriter` / `PbReader` the gRPC transport uses — with the field numbers
5+
// fixed by etcd's `rpc.proto`, so a `KeyValue` / `RangeRequest` / `PutRequest` encodes
6+
// byte-for-byte to what a real etcd server expects. The gRPC calls over an
7+
// `@moonrpc.Channel` build on top of this.
8+
9+
///|
10+
/// An etcd `KeyValue` (`mvccpb.KeyValue`): a key, its value, the create/mod revisions
11+
/// and version that track its history, and the lease it is attached to.
12+
pub(all) struct EtcdKeyValue {
13+
key : Bytes
14+
create_revision : Int64
15+
mod_revision : Int64
16+
version : Int64
17+
value : Bytes
18+
lease : Int64
19+
} derive(Eq)
20+
21+
///|
22+
/// The empty key/value with all-zero metadata.
23+
pub fn EtcdKeyValue::empty() -> EtcdKeyValue {
24+
{
25+
key: b"",
26+
create_revision: 0,
27+
mod_revision: 0,
28+
version: 0,
29+
value: b"",
30+
lease: 0,
31+
}
32+
}
33+
34+
///|
35+
/// Encode a `KeyValue` to its protobuf wire bytes (field numbers per etcd
36+
/// `mvccpb.proto`: key=1, create_revision=2, mod_revision=3, version=4, value=5,
37+
/// lease=6). Proto3 default (empty / zero) fields are omitted.
38+
pub fn EtcdKeyValue::encode(self : EtcdKeyValue) -> Bytes {
39+
let w = @moonrpc.PbWriter::new()
40+
if self.key.length() > 0 {
41+
w.bytes_(1, self.key)
42+
}
43+
if self.create_revision != 0 {
44+
w.int64(2, self.create_revision)
45+
}
46+
if self.mod_revision != 0 {
47+
w.int64(3, self.mod_revision)
48+
}
49+
if self.version != 0 {
50+
w.int64(4, self.version)
51+
}
52+
if self.value.length() > 0 {
53+
w.bytes_(5, self.value)
54+
}
55+
if self.lease != 0 {
56+
w.int64(6, self.lease)
57+
}
58+
w.to_bytes()
59+
}
60+
61+
///|
62+
/// Decode a `KeyValue` from protobuf wire bytes; unknown fields are skipped.
63+
pub fn EtcdKeyValue::decode(
64+
data : Bytes,
65+
) -> EtcdKeyValue raise @moonrpc.PbError {
66+
let r = @moonrpc.PbReader::new(data)
67+
let mut key = b""
68+
let mut create_revision = 0L
69+
let mut mod_revision = 0L
70+
let mut version = 0L
71+
let mut value = b""
72+
let mut lease = 0L
73+
while !r.eof() {
74+
let (field, wire) = r.read_tag()
75+
match field {
76+
1 => key = r.read_bytes()
77+
2 => create_revision = r.read_int64()
78+
3 => mod_revision = r.read_int64()
79+
4 => version = r.read_int64()
80+
5 => value = r.read_bytes()
81+
6 => lease = r.read_int64()
82+
_ => r.skip(wire)
83+
}
84+
}
85+
{ key, create_revision, mod_revision, version, value, lease }
86+
}
87+
88+
///|
89+
/// A `RangeRequest` (`etcdserverpb`): read the key at `key`, or the half-open range
90+
/// `[key, range_end)` when `range_end` is set, up to `limit` results (0 = no limit).
91+
pub(all) struct EtcdRangeRequest {
92+
key : Bytes
93+
range_end : Bytes
94+
limit : Int64
95+
} derive(Eq)
96+
97+
///|
98+
/// Encode a `RangeRequest` (key=1, range_end=2, limit=3).
99+
pub fn EtcdRangeRequest::encode(self : EtcdRangeRequest) -> Bytes {
100+
let w = @moonrpc.PbWriter::new()
101+
if self.key.length() > 0 {
102+
w.bytes_(1, self.key)
103+
}
104+
if self.range_end.length() > 0 {
105+
w.bytes_(2, self.range_end)
106+
}
107+
if self.limit != 0 {
108+
w.int64(3, self.limit)
109+
}
110+
w.to_bytes()
111+
}
112+
113+
///|
114+
/// Decode a `RangeRequest`.
115+
pub fn EtcdRangeRequest::decode(
116+
data : Bytes,
117+
) -> EtcdRangeRequest raise @moonrpc.PbError {
118+
let r = @moonrpc.PbReader::new(data)
119+
let mut key = b""
120+
let mut range_end = b""
121+
let mut limit = 0L
122+
while !r.eof() {
123+
let (field, wire) = r.read_tag()
124+
match field {
125+
1 => key = r.read_bytes()
126+
2 => range_end = r.read_bytes()
127+
3 => limit = r.read_int64()
128+
_ => r.skip(wire)
129+
}
130+
}
131+
{ key, range_end, limit }
132+
}
133+
134+
///|
135+
/// A `RangeResponse`: the matched key/values and the total `count` in the range (which
136+
/// may exceed the returned `kvs` when a `limit` capped them).
137+
pub(all) struct EtcdRangeResponse {
138+
kvs : Array[EtcdKeyValue]
139+
count : Int64
140+
} derive(Eq)
141+
142+
///|
143+
/// Encode a `RangeResponse` (kvs=2 repeated, count=4).
144+
pub fn EtcdRangeResponse::encode(self : EtcdRangeResponse) -> Bytes {
145+
let w = @moonrpc.PbWriter::new()
146+
for kv in self.kvs {
147+
w.message_(2, kv.encode())
148+
}
149+
if self.count != 0 {
150+
w.int64(4, self.count)
151+
}
152+
w.to_bytes()
153+
}
154+
155+
///|
156+
/// Decode a `RangeResponse`; each `kvs` entry is a nested `KeyValue` message.
157+
pub fn EtcdRangeResponse::decode(
158+
data : Bytes,
159+
) -> EtcdRangeResponse raise @moonrpc.PbError {
160+
let r = @moonrpc.PbReader::new(data)
161+
let kvs : Array[EtcdKeyValue] = []
162+
let mut count = 0L
163+
while !r.eof() {
164+
let (field, wire) = r.read_tag()
165+
match field {
166+
2 => kvs.push(EtcdKeyValue::decode(r.read_bytes()))
167+
4 => count = r.read_int64()
168+
_ => r.skip(wire)
169+
}
170+
}
171+
{ kvs, count }
172+
}
173+
174+
///|
175+
/// A `PutRequest`: store `value` at `key`, optionally under `lease`.
176+
pub(all) struct EtcdPutRequest {
177+
key : Bytes
178+
value : Bytes
179+
lease : Int64
180+
} derive(Eq)
181+
182+
///|
183+
/// Encode a `PutRequest` (key=1, value=2, lease=3).
184+
pub fn EtcdPutRequest::encode(self : EtcdPutRequest) -> Bytes {
185+
let w = @moonrpc.PbWriter::new()
186+
if self.key.length() > 0 {
187+
w.bytes_(1, self.key)
188+
}
189+
if self.value.length() > 0 {
190+
w.bytes_(2, self.value)
191+
}
192+
if self.lease != 0 {
193+
w.int64(3, self.lease)
194+
}
195+
w.to_bytes()
196+
}
197+
198+
///|
199+
/// Decode a `PutRequest`.
200+
pub fn EtcdPutRequest::decode(
201+
data : Bytes,
202+
) -> EtcdPutRequest raise @moonrpc.PbError {
203+
let r = @moonrpc.PbReader::new(data)
204+
let mut key = b""
205+
let mut value = b""
206+
let mut lease = 0L
207+
while !r.eof() {
208+
let (field, wire) = r.read_tag()
209+
match field {
210+
1 => key = r.read_bytes()
211+
2 => value = r.read_bytes()
212+
3 => lease = r.read_int64()
213+
_ => r.skip(wire)
214+
}
215+
}
216+
{ key, value, lease }
217+
}

etcd_wbtest.mbt

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Reference vectors produced by CPython's `protobuf` (6.33) compiling the etcd
2+
// `etcdserverpb` KV messages, so the self-built encoders must match the exact bytes a
3+
// real etcd server produces and reads.
4+
5+
///|
6+
fn etcd_nib(c : Char) -> Int {
7+
if c >= '0' && c <= '9' {
8+
c.to_int() - '0'.to_int()
9+
} else if c >= 'a' && c <= 'f' {
10+
c.to_int() - 'a'.to_int() + 10
11+
} else {
12+
c.to_int() - 'A'.to_int() + 10
13+
}
14+
}
15+
16+
///|
17+
fn etcd_unhex(s : String) -> Bytes {
18+
let buf = Buffer()
19+
let chars = s.to_array()
20+
let mut i = 0
21+
while i + 1 < chars.length() {
22+
buf.write_byte(
23+
((etcd_nib(chars[i]) << 4) | etcd_nib(chars[i + 1])).to_byte(),
24+
)
25+
i += 2
26+
}
27+
buf.to_bytes()
28+
}
29+
30+
///|
31+
test "etcd RangeRequest encodes to the exact protobuf bytes protobuf produces" {
32+
let req : EtcdRangeRequest = { key: b"foo", range_end: b"fop", limit: 100 }
33+
// protobuf: RangeRequest(key="foo", range_end="fop", limit=100)
34+
assert_eq(req.encode() == etcd_unhex("0a03666f6f1203666f701864"), true)
35+
assert_eq(EtcdRangeRequest::decode(req.encode()) == req, true)
36+
}
37+
38+
///|
39+
test "etcd KeyValue encodes to the exact protobuf bytes and round-trips" {
40+
let kv : EtcdKeyValue = {
41+
key: b"k",
42+
create_revision: 2,
43+
mod_revision: 3,
44+
version: 1,
45+
value: b"v",
46+
lease: 7,
47+
}
48+
// protobuf: KeyValue(key="k", create_revision=2, mod_revision=3, version=1,
49+
// value="v", lease=7)
50+
assert_eq(kv.encode() == etcd_unhex("0a016b1002180320012a01763007"), true)
51+
assert_eq(EtcdKeyValue::decode(kv.encode()) == kv, true)
52+
}
53+
54+
///|
55+
test "etcd PutRequest encodes to the exact protobuf bytes and round-trips" {
56+
let put : EtcdPutRequest = { key: b"k", value: b"v", lease: 7 }
57+
// protobuf: PutRequest(key="k", value="v", lease=7)
58+
assert_eq(put.encode() == etcd_unhex("0a016b1201761807"), true)
59+
assert_eq(EtcdPutRequest::decode(put.encode()) == put, true)
60+
}
61+
62+
///|
63+
test "etcd RangeResponse carries nested KeyValues and a count through a round-trip" {
64+
let resp : EtcdRangeResponse = {
65+
kvs: [
66+
{ ..EtcdKeyValue::empty(), key: b"a", value: b"1", mod_revision: 5 },
67+
{ ..EtcdKeyValue::empty(), key: b"b", value: b"2", mod_revision: 6 },
68+
],
69+
count: 2,
70+
}
71+
let decoded = EtcdRangeResponse::decode(resp.encode())
72+
assert_eq(decoded.count, 2)
73+
assert_eq(decoded.kvs.length(), 2)
74+
assert_eq(decoded.kvs[0].key == b"a", true)
75+
assert_eq(decoded.kvs[1].mod_revision, 6)
76+
assert_eq(decoded == resp, true)
77+
}
78+
79+
///|
80+
test "etcd default (empty/zero) fields are omitted, per proto3" {
81+
// A RangeRequest for a single key with no range_end or limit encodes only field 1.
82+
let req : EtcdRangeRequest = { key: b"x", range_end: b"", limit: 0 }
83+
assert_eq(req.encode() == etcd_unhex("0a0178"), true)
84+
}

moon.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ description = "moonzero — a service framework for MoonBit (← go-zero): confi
2222
import {
2323
"Lfan-ke/moonapi@0.6.2",
2424
"Lfan-ke/moonasgi@0.6.1",
25-
"Lfan-ke/moonrpc@0.5.0",
25+
"Lfan-ke/moonrpc@0.7.0",
2626
"moonbitlang/async@0.20.3",
2727
}

0 commit comments

Comments
 (0)