Skip to content

Commit e5810af

Browse files
committed
feat(moonzero): etcd Lease and watch Event protobuf messages.
The discovery path needs a lease for instance liveness and watch events for change notification. LeaseGrantRequest/Response, LeaseKeepAliveRequest/Response, and the mvccpb Event (PUT/DELETE with its KeyValue) now encode and decode over moonrpc's protobuf runtime with etcd's rpc.proto field numbers. Verified against reference vectors CPython's protobuf produced (mutation-checked on the proto3 default-omission of a PUT event's type) and round-tripped on all four backends. Signed-off-by: 林晨 (Leo Cheng) <chengkelfan@qq.com>
1 parent d4be14b commit e5810af

2 files changed

Lines changed: 274 additions & 0 deletions

File tree

etcd.mbt

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,232 @@ pub fn EtcdPutRequest::decode(
215215
}
216216
{ key, value, lease }
217217
}
218+
219+
///|
220+
/// A `LeaseGrantRequest`: ask etcd for a lease living `ttl` seconds (`id` 0 lets the
221+
/// server assign one). A registered service key attaches to the lease and vanishes
222+
/// when the lease expires — go-zero's instance liveness mechanism.
223+
pub(all) struct EtcdLeaseGrantRequest {
224+
ttl : Int64
225+
id : Int64
226+
} derive(Eq)
227+
228+
///|
229+
/// Encode a `LeaseGrantRequest` (TTL=1, ID=2).
230+
pub fn EtcdLeaseGrantRequest::encode(self : EtcdLeaseGrantRequest) -> Bytes {
231+
let w = @moonrpc.PbWriter::new()
232+
if self.ttl != 0 {
233+
w.int64(1, self.ttl)
234+
}
235+
if self.id != 0 {
236+
w.int64(2, self.id)
237+
}
238+
w.to_bytes()
239+
}
240+
241+
///|
242+
/// Decode a `LeaseGrantRequest`.
243+
pub fn EtcdLeaseGrantRequest::decode(
244+
data : Bytes,
245+
) -> EtcdLeaseGrantRequest raise @moonrpc.PbError {
246+
let r = @moonrpc.PbReader::new(data)
247+
let mut ttl = 0L
248+
let mut id = 0L
249+
while !r.eof() {
250+
let (field, wire) = r.read_tag()
251+
match field {
252+
1 => ttl = r.read_int64()
253+
2 => id = r.read_int64()
254+
_ => r.skip(wire)
255+
}
256+
}
257+
{ ttl, id }
258+
}
259+
260+
///|
261+
/// A `LeaseGrantResponse`: the granted lease `id`, its actual `ttl`, and an `error`
262+
/// string when the grant failed.
263+
pub(all) struct EtcdLeaseGrantResponse {
264+
id : Int64
265+
ttl : Int64
266+
error : String
267+
} derive(Eq)
268+
269+
///|
270+
/// Encode a `LeaseGrantResponse` (ID=2, TTL=3, error=4).
271+
pub fn EtcdLeaseGrantResponse::encode(self : EtcdLeaseGrantResponse) -> Bytes {
272+
let w = @moonrpc.PbWriter::new()
273+
if self.id != 0 {
274+
w.int64(2, self.id)
275+
}
276+
if self.ttl != 0 {
277+
w.int64(3, self.ttl)
278+
}
279+
if self.error.length() > 0 {
280+
w.string_(4, self.error)
281+
}
282+
w.to_bytes()
283+
}
284+
285+
///|
286+
/// Decode a `LeaseGrantResponse`.
287+
pub fn EtcdLeaseGrantResponse::decode(
288+
data : Bytes,
289+
) -> EtcdLeaseGrantResponse raise @moonrpc.PbError {
290+
let r = @moonrpc.PbReader::new(data)
291+
let mut id = 0L
292+
let mut ttl = 0L
293+
let mut error = ""
294+
while !r.eof() {
295+
let (field, wire) = r.read_tag()
296+
match field {
297+
2 => id = r.read_int64()
298+
3 => ttl = r.read_int64()
299+
4 => error = r.read_string()
300+
_ => r.skip(wire)
301+
}
302+
}
303+
{ id, ttl, error }
304+
}
305+
306+
///|
307+
/// A `LeaseKeepAliveRequest`: renew lease `id` before it expires. A discovery client
308+
/// streams these to keep its instance registered.
309+
pub(all) struct EtcdLeaseKeepAliveRequest {
310+
id : Int64
311+
} derive(Eq)
312+
313+
///|
314+
/// Encode a `LeaseKeepAliveRequest` (ID=1).
315+
pub fn EtcdLeaseKeepAliveRequest::encode(
316+
self : EtcdLeaseKeepAliveRequest,
317+
) -> Bytes {
318+
let w = @moonrpc.PbWriter::new()
319+
if self.id != 0 {
320+
w.int64(1, self.id)
321+
}
322+
w.to_bytes()
323+
}
324+
325+
///|
326+
/// Decode a `LeaseKeepAliveRequest`.
327+
pub fn EtcdLeaseKeepAliveRequest::decode(
328+
data : Bytes,
329+
) -> EtcdLeaseKeepAliveRequest raise @moonrpc.PbError {
330+
let r = @moonrpc.PbReader::new(data)
331+
let mut id = 0L
332+
while !r.eof() {
333+
let (field, wire) = r.read_tag()
334+
match field {
335+
1 => id = r.read_int64()
336+
_ => r.skip(wire)
337+
}
338+
}
339+
{ id, }
340+
}
341+
342+
///|
343+
/// A `LeaseKeepAliveResponse`: the renewed lease `id` and its remaining `ttl` (0 =
344+
/// the lease has expired).
345+
pub(all) struct EtcdLeaseKeepAliveResponse {
346+
id : Int64
347+
ttl : Int64
348+
} derive(Eq)
349+
350+
///|
351+
/// Encode a `LeaseKeepAliveResponse` (ID=2, TTL=3).
352+
pub fn EtcdLeaseKeepAliveResponse::encode(
353+
self : EtcdLeaseKeepAliveResponse,
354+
) -> Bytes {
355+
let w = @moonrpc.PbWriter::new()
356+
if self.id != 0 {
357+
w.int64(2, self.id)
358+
}
359+
if self.ttl != 0 {
360+
w.int64(3, self.ttl)
361+
}
362+
w.to_bytes()
363+
}
364+
365+
///|
366+
/// Decode a `LeaseKeepAliveResponse`.
367+
pub fn EtcdLeaseKeepAliveResponse::decode(
368+
data : Bytes,
369+
) -> EtcdLeaseKeepAliveResponse raise @moonrpc.PbError {
370+
let r = @moonrpc.PbReader::new(data)
371+
let mut id = 0L
372+
let mut ttl = 0L
373+
while !r.eof() {
374+
let (field, wire) = r.read_tag()
375+
match field {
376+
2 => id = r.read_int64()
377+
3 => ttl = r.read_int64()
378+
_ => r.skip(wire)
379+
}
380+
}
381+
{ id, ttl }
382+
}
383+
384+
///|
385+
/// The kind of change a watch `Event` reports (`mvccpb.Event.EventType`): a key was
386+
/// `Put` (created or updated) or `Delete`d.
387+
pub(all) enum EtcdEventType {
388+
Put
389+
Delete
390+
} derive(Eq)
391+
392+
///|
393+
/// The protobuf enum number of an event type (`PUT` = 0, `DELETE` = 1).
394+
pub fn EtcdEventType::to_int(self : EtcdEventType) -> Int {
395+
match self {
396+
Put => 0
397+
Delete => 1
398+
}
399+
}
400+
401+
///|
402+
/// The event type for a protobuf enum number; unknown numbers read as `Put`.
403+
pub fn EtcdEventType::from_int(n : Int) -> EtcdEventType {
404+
if n == 1 {
405+
Delete
406+
} else {
407+
Put
408+
}
409+
}
410+
411+
///|
412+
/// A watch `Event` (`mvccpb.Event`): a change to one key, carrying the resulting
413+
/// `KeyValue` (for a delete, the key with cleared metadata). This is what a discovery
414+
/// watcher folds into add/remove of a service instance.
415+
pub(all) struct EtcdEvent {
416+
event_type : EtcdEventType
417+
kv : EtcdKeyValue
418+
} derive(Eq)
419+
420+
///|
421+
/// Encode an `Event` (type=1, kv=2). `PUT` (0) is the proto3 default and omitted.
422+
pub fn EtcdEvent::encode(self : EtcdEvent) -> Bytes {
423+
let w = @moonrpc.PbWriter::new()
424+
if self.event_type.to_int() != 0 {
425+
w.int32(1, self.event_type.to_int())
426+
}
427+
w.message_(2, self.kv.encode())
428+
w.to_bytes()
429+
}
430+
431+
///|
432+
/// Decode an `Event`.
433+
pub fn EtcdEvent::decode(data : Bytes) -> EtcdEvent raise @moonrpc.PbError {
434+
let r = @moonrpc.PbReader::new(data)
435+
let mut event_type = EtcdEventType::Put
436+
let mut kv = EtcdKeyValue::empty()
437+
while !r.eof() {
438+
let (field, wire) = r.read_tag()
439+
match field {
440+
1 => event_type = EtcdEventType::from_int(r.read_int32())
441+
2 => kv = EtcdKeyValue::decode(r.read_bytes())
442+
_ => r.skip(wire)
443+
}
444+
}
445+
{ event_type, kv }
446+
}

etcd_wbtest.mbt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,48 @@ test "etcd default (empty/zero) fields are omitted, per proto3" {
8282
let req : EtcdRangeRequest = { key: b"x", range_end: b"", limit: 0 }
8383
assert_eq(req.encode() == etcd_unhex("0a0178"), true)
8484
}
85+
86+
///|
87+
test "etcd Lease messages encode to the exact protobuf bytes and round-trip" {
88+
let grant : EtcdLeaseGrantRequest = { ttl: 60, id: 1234 }
89+
assert_eq(grant.encode() == etcd_unhex("083c10d209"), true)
90+
assert_eq(EtcdLeaseGrantRequest::decode(grant.encode()) == grant, true)
91+
let granted : EtcdLeaseGrantResponse = { id: 1234, ttl: 60, error: "" }
92+
assert_eq(granted.encode() == etcd_unhex("10d209183c"), true)
93+
assert_eq(EtcdLeaseGrantResponse::decode(granted.encode()) == granted, true)
94+
let ka : EtcdLeaseKeepAliveRequest = { id: 1234 }
95+
assert_eq(ka.encode() == etcd_unhex("08d209"), true)
96+
assert_eq(EtcdLeaseKeepAliveRequest::decode(ka.encode()) == ka, true)
97+
let kar : EtcdLeaseKeepAliveResponse = { id: 1234, ttl: 59 }
98+
assert_eq(kar.encode() == etcd_unhex("10d209183b"), true)
99+
assert_eq(EtcdLeaseKeepAliveResponse::decode(kar.encode()) == kar, true)
100+
}
101+
102+
///|
103+
test "etcd watch Event encodes to the exact protobuf bytes, PUT vs DELETE" {
104+
// A PUT event carries the full key/value; type PUT (0) is the proto3 default so it
105+
// is omitted from the wire.
106+
let put : EtcdEvent = {
107+
event_type: Put,
108+
kv: {
109+
..EtcdKeyValue::empty(),
110+
key: b"svc/a",
111+
value: b"1.2.3.4",
112+
mod_revision: 9,
113+
},
114+
}
115+
assert_eq(
116+
put.encode() == etcd_unhex("12120a057376632f6118092a07312e322e332e34"),
117+
true,
118+
)
119+
assert_eq(EtcdEvent::decode(put.encode()) == put, true)
120+
// A DELETE event writes the type explicitly and just the key.
121+
let del : EtcdEvent = {
122+
event_type: Delete,
123+
kv: { ..EtcdKeyValue::empty(), key: b"svc/a" },
124+
}
125+
assert_eq(del.encode() == etcd_unhex("080112070a057376632f61"), true)
126+
let decoded = EtcdEvent::decode(del.encode())
127+
assert_eq(decoded.event_type == Delete, true)
128+
assert_eq(decoded.kv.key == b"svc/a", true)
129+
}

0 commit comments

Comments
 (0)