Skip to content

Commit 9a5379e

Browse files
committed
feat(moonzero): etcd Watch protobuf messages complete the discovery message layer.
WatchCreateRequest, WatchCancelRequest, the WatchRequest request_union oneof (modelled as a MoonBit enum), and WatchResponse (lifecycle flags plus its repeated Events) round out the etcd v3 message set the discovery client needs: Range to list, Put + Lease to register with a TTL, and Watch to observe changes. All encode/decode over moonrpc's protobuf runtime with etcd's field numbers, verified against CPython protobuf reference vectors — including the oneof wrapping, a repeated events field, and bool flags — and round-tripped on all four backends. Signed-off-by: 林晨 (Leo Cheng) <chengkelfan@qq.com>
1 parent e5810af commit 9a5379e

2 files changed

Lines changed: 223 additions & 0 deletions

File tree

etcd.mbt

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,3 +444,178 @@ pub fn EtcdEvent::decode(data : Bytes) -> EtcdEvent raise @moonrpc.PbError {
444444
}
445445
{ event_type, kv }
446446
}
447+
448+
///|
449+
/// A `WatchCreateRequest`: subscribe to changes on `key`, or on the half-open range
450+
/// `[key, range_end)`, from `start_revision` (0 = current). A discovery watcher opens
451+
/// one over the service's key prefix.
452+
pub(all) struct EtcdWatchCreateRequest {
453+
key : Bytes
454+
range_end : Bytes
455+
start_revision : Int64
456+
} derive(Eq)
457+
458+
///|
459+
/// Encode a `WatchCreateRequest` (key=1, range_end=2, start_revision=3).
460+
pub fn EtcdWatchCreateRequest::encode(self : EtcdWatchCreateRequest) -> Bytes {
461+
let w = @moonrpc.PbWriter::new()
462+
if self.key.length() > 0 {
463+
w.bytes_(1, self.key)
464+
}
465+
if self.range_end.length() > 0 {
466+
w.bytes_(2, self.range_end)
467+
}
468+
if self.start_revision != 0 {
469+
w.int64(3, self.start_revision)
470+
}
471+
w.to_bytes()
472+
}
473+
474+
///|
475+
/// Decode a `WatchCreateRequest`.
476+
pub fn EtcdWatchCreateRequest::decode(
477+
data : Bytes,
478+
) -> EtcdWatchCreateRequest raise @moonrpc.PbError {
479+
let r = @moonrpc.PbReader::new(data)
480+
let mut key = b""
481+
let mut range_end = b""
482+
let mut start_revision = 0L
483+
while !r.eof() {
484+
let (field, wire) = r.read_tag()
485+
match field {
486+
1 => key = r.read_bytes()
487+
2 => range_end = r.read_bytes()
488+
3 => start_revision = r.read_int64()
489+
_ => r.skip(wire)
490+
}
491+
}
492+
{ key, range_end, start_revision }
493+
}
494+
495+
///|
496+
/// A `WatchCancelRequest`: stop the watch stream identified by `watch_id`.
497+
pub(all) struct EtcdWatchCancelRequest {
498+
watch_id : Int64
499+
} derive(Eq)
500+
501+
///|
502+
/// Encode a `WatchCancelRequest` (watch_id=1).
503+
pub fn EtcdWatchCancelRequest::encode(self : EtcdWatchCancelRequest) -> Bytes {
504+
let w = @moonrpc.PbWriter::new()
505+
if self.watch_id != 0 {
506+
w.int64(1, self.watch_id)
507+
}
508+
w.to_bytes()
509+
}
510+
511+
///|
512+
/// Decode a `WatchCancelRequest`.
513+
pub fn EtcdWatchCancelRequest::decode(
514+
data : Bytes,
515+
) -> EtcdWatchCancelRequest raise @moonrpc.PbError {
516+
let r = @moonrpc.PbReader::new(data)
517+
let mut watch_id = 0L
518+
while !r.eof() {
519+
let (field, wire) = r.read_tag()
520+
match field {
521+
1 => watch_id = r.read_int64()
522+
_ => r.skip(wire)
523+
}
524+
}
525+
{ watch_id, }
526+
}
527+
528+
///|
529+
/// A `WatchRequest`, the `request_union` oneof of the bidi Watch stream: either a
530+
/// `Create` to open a watch or a `Cancel` to close one.
531+
pub(all) enum EtcdWatchRequest {
532+
Create(EtcdWatchCreateRequest)
533+
Cancel(EtcdWatchCancelRequest)
534+
} derive(Eq)
535+
536+
///|
537+
/// Encode a `WatchRequest` (create_request=1, cancel_request=2).
538+
pub fn EtcdWatchRequest::encode(self : EtcdWatchRequest) -> Bytes {
539+
let w = @moonrpc.PbWriter::new()
540+
match self {
541+
Create(c) => w.message_(1, c.encode())
542+
Cancel(c) => w.message_(2, c.encode())
543+
}
544+
w.to_bytes()
545+
}
546+
547+
///|
548+
/// Decode a `WatchRequest`; the last-set oneof arm wins, defaulting to an empty
549+
/// `Create`.
550+
pub fn EtcdWatchRequest::decode(
551+
data : Bytes,
552+
) -> EtcdWatchRequest raise @moonrpc.PbError {
553+
let r = @moonrpc.PbReader::new(data)
554+
let mut result : EtcdWatchRequest = Create({
555+
key: b"",
556+
range_end: b"",
557+
start_revision: 0,
558+
})
559+
while !r.eof() {
560+
let (field, wire) = r.read_tag()
561+
match field {
562+
1 => result = Create(EtcdWatchCreateRequest::decode(r.read_bytes()))
563+
2 => result = Cancel(EtcdWatchCancelRequest::decode(r.read_bytes()))
564+
_ => r.skip(wire)
565+
}
566+
}
567+
result
568+
}
569+
570+
///|
571+
/// A `WatchResponse`: the server-assigned `watch_id`, the `created` / `canceled`
572+
/// lifecycle flags, and the batch of change `events` since the last response. A
573+
/// discovery watcher folds each event into add/remove of a service instance.
574+
pub(all) struct EtcdWatchResponse {
575+
watch_id : Int64
576+
created : Bool
577+
canceled : Bool
578+
events : Array[EtcdEvent]
579+
} derive(Eq)
580+
581+
///|
582+
/// Encode a `WatchResponse` (watch_id=2, created=3, canceled=4, events=11 repeated).
583+
pub fn EtcdWatchResponse::encode(self : EtcdWatchResponse) -> Bytes {
584+
let w = @moonrpc.PbWriter::new()
585+
if self.watch_id != 0 {
586+
w.int64(2, self.watch_id)
587+
}
588+
if self.created {
589+
w.bool_(3, self.created)
590+
}
591+
if self.canceled {
592+
w.bool_(4, self.canceled)
593+
}
594+
for ev in self.events {
595+
w.message_(11, ev.encode())
596+
}
597+
w.to_bytes()
598+
}
599+
600+
///|
601+
/// Decode a `WatchResponse`.
602+
pub fn EtcdWatchResponse::decode(
603+
data : Bytes,
604+
) -> EtcdWatchResponse raise @moonrpc.PbError {
605+
let r = @moonrpc.PbReader::new(data)
606+
let mut watch_id = 0L
607+
let mut created = false
608+
let mut canceled = false
609+
let events : Array[EtcdEvent] = []
610+
while !r.eof() {
611+
let (field, wire) = r.read_tag()
612+
match field {
613+
2 => watch_id = r.read_int64()
614+
3 => created = r.read_bool()
615+
4 => canceled = r.read_bool()
616+
11 => events.push(EtcdEvent::decode(r.read_bytes()))
617+
_ => r.skip(wire)
618+
}
619+
}
620+
{ watch_id, created, canceled, events }
621+
}

etcd_wbtest.mbt

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,51 @@ test "etcd watch Event encodes to the exact protobuf bytes, PUT vs DELETE" {
127127
assert_eq(decoded.event_type == Delete, true)
128128
assert_eq(decoded.kv.key == b"svc/a", true)
129129
}
130+
131+
///|
132+
test "etcd Watch create/cancel requests encode to the exact protobuf bytes" {
133+
let wc : EtcdWatchCreateRequest = {
134+
key: b"svc/",
135+
range_end: b"svc0",
136+
start_revision: 5,
137+
}
138+
assert_eq(wc.encode() == etcd_unhex("0a047376632f1204737663301805"), true)
139+
assert_eq(EtcdWatchCreateRequest::decode(wc.encode()) == wc, true)
140+
// wrapped in the oneof WatchRequest as create_request (field 1)
141+
let create_req : EtcdWatchRequest = Create(wc)
142+
assert_eq(
143+
create_req.encode() == etcd_unhex("0a0e0a047376632f1204737663301805"),
144+
true,
145+
)
146+
assert_eq(EtcdWatchRequest::decode(create_req.encode()) == create_req, true)
147+
// cancel_request (field 2)
148+
let cancel_req : EtcdWatchRequest = Cancel({ watch_id: 42 })
149+
assert_eq(cancel_req.encode() == etcd_unhex("1202082a"), true)
150+
assert_eq(EtcdWatchRequest::decode(cancel_req.encode()) == cancel_req, true)
151+
}
152+
153+
///|
154+
test "etcd WatchResponse carries lifecycle flags and events through a round-trip" {
155+
let resp : EtcdWatchResponse = {
156+
watch_id: 7,
157+
created: true,
158+
canceled: false,
159+
events: [
160+
{
161+
event_type: Put,
162+
kv: { ..EtcdKeyValue::empty(), key: b"svc/a", value: b"h1" },
163+
},
164+
],
165+
}
166+
// protobuf: WatchResponse(watch_id=7, created=true, events=[PUT svc/a=h1])
167+
assert_eq(
168+
resp.encode() == etcd_unhex("100718015a0d120b0a057376632f612a026831"),
169+
true,
170+
)
171+
let decoded = EtcdWatchResponse::decode(resp.encode())
172+
assert_eq(decoded.watch_id, 7)
173+
assert_eq(decoded.created, true)
174+
assert_eq(decoded.events.length(), 1)
175+
assert_eq(decoded.events[0].kv.key == b"svc/a", true)
176+
assert_eq(decoded == resp, true)
177+
}

0 commit comments

Comments
 (0)