Skip to content

Commit c422cf5

Browse files
send metadata with channel request
1 parent 1dddefc commit c422cf5

11 files changed

Lines changed: 111 additions & 58 deletions

File tree

PROTOCOL.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ To open a channel to a service, the client initiates a new HTTP/2 stream with th
4949
* `:path`: `/channel`
5050
* `x-service-name`: `{local_service_name}` (Name of the service initiating the connection)
5151
* `x-session-id`: `{optional_session_id}`
52+
* `x-service-name`: `{local_service_name}`
53+
54+
Any additional headers sent during stream initiation or unary calls are exposed to the receiving handler as metadata.
5255

5356
The server accepts the stream by sending headers:
5457
* `:status`: `200`

node/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ Register a service name to handle incoming connections.
4343
```typescript
4444
mesh.registerService('my-service-name')
4545
// 1. Streaming Handler
46-
.onRequestChannel(async (channel) => {
47-
console.log(`Accepted connection`);
46+
.onRequestChannel(async (channel, metadata) => {
47+
console.log(`Accepted connection from session: ${metadata?.['x-session-id']}`);
4848
try {
4949
await channel.onData(async (msg) => {
5050
console.log('Received:', msg.payload);
@@ -55,8 +55,9 @@ mesh.registerService('my-service-name')
5555
}
5656
})
5757
// 2. Unary Handler (Request/Reply)
58-
.onRequestReply(async (msg) => {
58+
.onRequestReply(async (msg, metadata) => {
5959
console.log('Received Unary:', msg.payload);
60+
console.log('Metadata:', metadata);
6061
return { function_name: 'reply', payload: 'Got Unary!' };
6162
});
6263
```

node/src/abstract.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ export interface ICommunication {
2424
// Python ICommunication has: create_channel, listen_channel, start.
2525
// It doesn't explicitly have close() in standard steps but Mesh.close might need it.
2626

27-
createChannel(node: Node): Promise<IChannel>;
28-
listenChannel(serviceName: string, callback: (channel: IChannel) => void): void;
27+
createChannel(node: Node, metadata?: Record<string, string>): Promise<IChannel>;
28+
listenChannel(serviceName: string, callback: (channel: IChannel, metadata?: Record<string, string>) => void): void;
2929

3030
// Unary support
31-
sendUnary(node: Node, message: any): Promise<any>;
32-
listenUnary(serviceName: string, callback: (msg: any) => Promise<any>): void;
31+
sendUnary(node: Node, message: any, metadata?: Record<string, string>): Promise<any>;
32+
listenUnary(serviceName: string, callback: (msg: any, metadata?: Record<string, string>) => Promise<any>): void;
3333
}
3434

3535
export interface IServiceDiscovery {
@@ -63,13 +63,13 @@ export interface IPubSub {
6363
}
6464

6565
export interface IServiceHandlers {
66-
onRequestChannel?: (channel: IChannel) => void;
67-
onRequestReply?: (msg: any) => Promise<any>;
66+
onRequestChannel?: (channel: IChannel, metadata?: Record<string, string>) => void;
67+
onRequestReply?: (msg: any, metadata?: Record<string, string>) => Promise<any>;
6868
}
6969

7070
export interface IServiceRegistration {
71-
onRequestChannel(handler: (channel: IChannel) => void): IServiceRegistration;
72-
onRequestReply(handler: (msg: any) => Promise<any>): IServiceRegistration;
71+
onRequestChannel(handler: (channel: IChannel, metadata?: Record<string, string>) => void): IServiceRegistration;
72+
onRequestReply(handler: (msg: any, metadata?: Record<string, string>) => Promise<any>): IServiceRegistration;
7373
}
7474

7575
export interface IMesh {

node/src/communication/grpc-communication.ts

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ export class GrpcChannel implements IChannel {
123123
export class GrpcCommunication extends EventEmitter implements ICommunication, IMeshServiceServer {
124124
private config: MeshConfig;
125125
private server: grpc.Server;
126-
private channelListeners: { [service: string]: (channel: IChannel) => void } = {};
127-
private unaryListeners: { [service: string]: (msg: any) => Promise<any> } = {};
126+
private channelListeners: { [service: string]: (channel: IChannel, metadata?: Record<string, string>) => void } = {};
127+
private unaryListeners: { [service: string]: (msg: any, metadata?: Record<string, string>) => Promise<any> } = {};
128128

129129
// Note: IMeshServiceServer methods are implemented as arrow functions or bound methods usually.
130130
// Or we simply pass implementation object.
@@ -164,9 +164,18 @@ export class GrpcCommunication extends EventEmitter implements ICommunication, I
164164
const metadata = call.metadata;
165165
const serviceName = metadata.get('x-service-name')[0] as string;
166166

167+
// Convert grpc Metadata to Record<string, string>
168+
const metadataRecord: Record<string, string> = {};
169+
for (const key in metadata.getMap()) {
170+
const val = metadata.get(key);
171+
if (val.length > 0) {
172+
metadataRecord[key] = val[0] as string;
173+
}
174+
}
175+
167176
if (serviceName && this.channelListeners[serviceName]) {
168177
const channel = new GrpcChannel(call);
169-
this.channelListeners[serviceName](channel);
178+
this.channelListeners[serviceName](channel, metadataRecord);
170179
} else {
171180
console.warn(`[gRPC] No channel listener for service: ${serviceName}`);
172181
call.end();
@@ -177,14 +186,23 @@ export class GrpcCommunication extends EventEmitter implements ICommunication, I
177186
const metadata = call.metadata;
178187
const serviceName = metadata.get('x-service-name')[0] as string;
179188

189+
// Convert grpc Metadata to Record<string, string>
190+
const metadataRecord: Record<string, string> = {};
191+
for (const key in metadata.getMap()) {
192+
const val = metadata.get(key);
193+
if (val.length > 0) {
194+
metadataRecord[key] = val[0] as string;
195+
}
196+
}
197+
180198
if (serviceName && this.unaryListeners[serviceName]) {
181199
const requestMsg = call.request;
182200
try {
183201
const payloadBytes = requestMsg.getPayload_asU8();
184202
const jsonString = Buffer.from(payloadBytes).toString('utf8');
185203
const msg = JSON.parse(jsonString);
186204

187-
this.unaryListeners[serviceName](msg)
205+
this.unaryListeners[serviceName](msg, metadataRecord)
188206
.then(response => {
189207
const payload = Buffer.from(JSON.stringify(response), 'utf8');
190208
const respMsg = new MeshMessage();
@@ -215,38 +233,50 @@ export class GrpcCommunication extends EventEmitter implements ICommunication, I
215233
}
216234
}
217235

218-
public listenChannel(serviceName: string, callback: (channel: IChannel) => void) {
236+
public listenChannel(serviceName: string, callback: (channel: IChannel, metadata?: Record<string, string>) => void) {
219237
this.channelListeners[serviceName] = callback;
220238
}
221239

222-
public listenUnary(serviceName: string, callback: (msg: any) => Promise<any>) {
240+
public listenUnary(serviceName: string, callback: (msg: any, metadata?: Record<string, string>) => Promise<any>) {
223241
this.unaryListeners[serviceName] = callback;
224242
}
225243

226-
public async createChannel(node: Node): Promise<IChannel> {
244+
public async createChannel(node: Node, metadata?: Record<string, string>): Promise<IChannel> {
227245
const address = `${node.host}:${node.port}`;
228246
const client = new MeshServiceClient(address, grpc.credentials.createInsecure());
229247

230-
const metadata = new grpc.Metadata();
231-
metadata.add('x-service-name', node.service_name);
248+
const grpcMetadata = new grpc.Metadata();
249+
grpcMetadata.add('x-service-name', node.service_name);
232250

233-
const call = client.send(metadata);
251+
if (metadata) {
252+
for (const [key, value] of Object.entries(metadata)) {
253+
grpcMetadata.add(key, value);
254+
}
255+
}
256+
257+
const call = client.send(grpcMetadata);
234258
return new GrpcChannel(call);
235259
}
236260

237-
public async sendUnary(node: Node, message: any): Promise<any> {
261+
public async sendUnary(node: Node, message: any, metadata?: Record<string, string>): Promise<any> {
238262
return new Promise((resolve, reject) => {
239263
const address = `${node.host}:${node.port}`;
240264
const client = new MeshServiceClient(address, grpc.credentials.createInsecure());
241265

242-
const metadata = new grpc.Metadata();
243-
metadata.add('x-service-name', node.service_name);
266+
const grpcMetadata = new grpc.Metadata();
267+
grpcMetadata.add('x-service-name', node.service_name);
268+
269+
if (metadata) {
270+
for (const [key, value] of Object.entries(metadata)) {
271+
grpcMetadata.add(key, value);
272+
}
273+
}
244274

245275
const payload = Buffer.from(JSON.stringify(message), 'utf8');
246276
const reqMsg = new MeshMessage();
247277
reqMsg.setPayload(payload);
248278

249-
client.unary(reqMsg, metadata, (err: grpc.ServiceError | null, response: MeshMessage) => {
279+
client.unary(reqMsg, grpcMetadata, (err: grpc.ServiceError | null, response: MeshMessage) => {
250280
if (err) {
251281
reject(err);
252282
} else {

node/src/mesh.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ export class Mesh implements IMesh {
5353

5454
public registerService(serviceName: string): IServiceRegistration {
5555
const registration = {
56-
onRequestChannel: (handler: (channel: IChannel) => void) => {
56+
onRequestChannel: (handler: (channel: IChannel, metadata?: Record<string, string>) => void) => {
5757
this.communication.listenChannel(serviceName, handler);
5858
return registration;
5959
},
60-
onRequestReply: (handler: (msg: any) => Promise<any>) => {
60+
onRequestReply: (handler: (msg: any, metadata?: Record<string, string>) => Promise<any>) => {
6161
this.communication.listenUnary(serviceName, handler);
6262
return registration;
6363
}

node/src/service-client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export class ServiceClient implements IServiceClient {
2323
throw new Error("Failed to map session to node");
2424
}
2525

26-
return this.communication.sendUnary(targetNode, message);
26+
return this.communication.sendUnary(targetNode, message, { 'x-session-id': sessionId });
2727
}
2828

2929
public async requestChannel(sessionId: string): Promise<IChannel> {
@@ -40,6 +40,6 @@ export class ServiceClient implements IServiceClient {
4040
}
4141

4242
console.log(`[Mesh] Mapping session '${sessionId}' -> Node ${targetNode.id}`);
43-
return this.communication.createChannel(targetNode);
43+
return this.communication.createChannel(targetNode, { 'x-session-id': sessionId });
4444
}
4545
}

python/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ if __name__ == "__main__":
5151
Register a service name to handle incoming connections.
5252

5353
```python
54-
async def stream_handler(channel):
55-
print("Accepted connection")
54+
async def stream_handler(channel, metadata):
55+
print(f"Accepted connection from session: {metadata.get('x-session-id')}")
5656
try:
5757
async def handler(msg):
5858
print(f"Received: {msg['payload']}")
@@ -63,8 +63,9 @@ async def stream_handler(channel):
6363
except Exception:
6464
pass
6565

66-
async def unary_handler(msg):
66+
async def unary_handler(msg, metadata):
6767
print(f"Received Unary: {msg['payload']}")
68+
print(f"Metadata: {metadata}")
6869
return {"function_name": "reply", "payload": "Got Unary!"}
6970

7071
mesh.register_service("my-service-name") \

python/src/mesh/abstract.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from abc import ABC, abstractmethod
2-
from typing import Callable, Awaitable, List, Optional, Any, TypedDict, Union
2+
from typing import Callable, Awaitable, List, Optional, Any, TypedDict, Union, Dict
33
from mesh.shared.types import Node
44

55
class IServiceHandlers(TypedDict, total=False):
6-
on_request_channel: Optional[Callable[['IChannel'], Awaitable[None]]]
7-
on_request_reply: Optional[Callable[[Any], Awaitable[Any]]]
6+
on_request_channel: Optional[Callable[['IChannel', Dict[str, str]], Awaitable[None]]]
7+
on_request_reply: Optional[Callable[[Any, Dict[str, str]], Awaitable[Any]]]
88

99
class IServiceClient(ABC):
1010
@abstractmethod
@@ -104,27 +104,38 @@ async def on_data(self, handler: Callable[[Any], Awaitable[None]]):
104104

105105
class ICommunication(ABC):
106106
@abstractmethod
107-
async def create_channel(self, target: Union[str, Node]) -> IChannel:
107+
async def create_channel(self, target: Union[str, Node], metadata: Dict[str, str] = None) -> IChannel:
108108
"""
109109
Open a new channel to a service or specific node.
110110
"""
111111
pass
112112

113113
@abstractmethod
114-
def listen_channel(self, service_name: str, callback: Callable[[IChannel], Awaitable[None]]):
114+
def listen_channel(self, service_name: str, callback: Callable[[IChannel, Dict[str, str]], Awaitable[None]]):
115115
"""
116116
Register a callback to handle incoming channels for a service.
117117
The callback receives a IChannel object to read/write messages.
118118
"""
119119
pass
120120

121121
@abstractmethod
122-
async def start(self):
122+
async def send_unary(self, node: Node, message: Any, metadata: Dict[str, str] = None) -> Any:
123+
"""
124+
Send a unary message to a node.
125+
"""
126+
pass
127+
128+
@abstractmethod
129+
def listen_unary(self, service_name: str, callback: Callable[[Any, Dict[str, str]], Awaitable[Any]]):
123130
"""
124-
Start the communication server.
131+
Register a callback to handle unary messages.
125132
"""
126133
pass
127134

135+
@abstractmethod
136+
async def start(self):
137+
pass
138+
128139
class IServiceDiscovery(ABC):
129140
@abstractmethod
130141
def register_service(self, node: Node):
@@ -151,11 +162,11 @@ def get_service(self, service_name: str) -> List[Node]:
151162

152163
class IServiceRegistration(ABC):
153164
@abstractmethod
154-
def on_request_channel(self, handler: Callable[['IChannel'], Awaitable[None]]) -> 'IServiceRegistration':
165+
def on_request_channel(self, handler: Callable[['IChannel', Dict[str, str]], Awaitable[None]]) -> 'IServiceRegistration':
155166
pass
156167

157168
@abstractmethod
158-
def on_request_reply(self, handler: Callable[[Any], Awaitable[Any]]) -> 'IServiceRegistration':
169+
def on_request_reply(self, handler: Callable[[Any, Dict[str, str]], Awaitable[Any]]) -> 'IServiceRegistration':
159170
pass
160171

161172
class IMesh(ABC):

python/src/mesh/communication/grpc_communication.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ async def Send(self, request_iterator, context):
9898
channel = GrpcChannel(request_iterator, outgoing_queue)
9999

100100
# Notify listener
101-
asyncio.create_task(self.comm.listeners[service_name](channel))
101+
asyncio.create_task(self.comm.listeners[service_name](channel, metadata))
102102

103103
# Stream outgoing messages back to client
104104
while True:
@@ -118,7 +118,7 @@ async def Unary(self, request, context):
118118
if service_name and service_name in self.comm.unary_listeners:
119119
try:
120120
obj = json.loads(request.payload.decode('utf-8'))
121-
response_obj = await self.comm.unary_listeners[service_name](obj)
121+
response_obj = await self.comm.unary_listeners[service_name](obj, metadata)
122122

123123
payload = json.dumps(response_obj).encode('utf-8')
124124
return mesh_pb2.MeshMessage(payload=payload)
@@ -137,8 +137,9 @@ class GrpcCommunication(ICommunication):
137137
def __init__(self, config: MeshConfig, service_discovery: IServiceDiscovery):
138138
self.config = config
139139
self.service_discovery = service_discovery
140-
self.listeners: Dict[str, Callable[[IChannel], Awaitable[None]]] = {}
141-
self.unary_listeners: Dict[str, Callable[[Any], Awaitable[Any]]] = {}
140+
self.service_discovery = service_discovery
141+
self.listeners: Dict[str, Callable[[IChannel, Dict[str, str]], Awaitable[None]]] = {}
142+
self.unary_listeners: Dict[str, Callable[[Any, Dict[str, str]], Awaitable[Any]]] = {}
142143
self._server = None
143144

144145
async def start(self):
@@ -163,13 +164,13 @@ async def start(self):
163164
# But if main program exits, it stops.
164165
# Mesh start() usually returns. The server keeps running attached to loop.
165166

166-
def listen_channel(self, service_name: str, callback: Callable[[IChannel], Awaitable[None]]):
167+
def listen_channel(self, service_name: str, callback: Callable[[IChannel, Dict[str, str]], Awaitable[None]]):
167168
self.listeners[service_name] = callback
168169

169-
def listen_unary(self, service_name: str, callback: Callable[[Any], Awaitable[Any]]):
170+
def listen_unary(self, service_name: str, callback: Callable[[Any, Dict[str, str]], Awaitable[Any]]):
170171
self.unary_listeners[service_name] = callback
171172

172-
async def create_channel(self, target: Union[str, Node]) -> IChannel:
173+
async def create_channel(self, target: Union[str, Node], metadata: Dict[str, str] = None) -> IChannel:
173174
if isinstance(target, str):
174175
service_name = target
175176
nodes = self.service_discovery.get_service(service_name)
@@ -193,22 +194,28 @@ async def request_generator():
193194
return
194195
yield msg
195196

196-
metadata = [('x-service-name', node.service_name)]
197+
metadata_list = [('x-service-name', node.service_name)]
198+
if metadata:
199+
for k, v in metadata.items():
200+
metadata_list.append((k, v))
197201

198-
response_iterator = stub.Send(request_generator(), metadata=metadata)
202+
response_iterator = stub.Send(request_generator(), metadata=metadata_list)
199203

200204
return GrpcChannel(response_iterator, outgoing_queue)
201205

202-
async def send_unary(self, node: Node, message: Any) -> Any:
206+
async def send_unary(self, node: Node, message: Any, metadata: Dict[str, str] = None) -> Any:
203207
channel_creds = grpc.aio.insecure_channel(f'{node.host}:{node.port}')
204208
stub = mesh_pb2_grpc.MeshServiceStub(channel_creds)
205209

206210
try:
207211
payload = json.dumps(message).encode('utf-8')
208212
req = mesh_pb2.MeshMessage(payload=payload)
209-
metadata = [('x-service-name', node.service_name)]
213+
metadata_list = [('x-service-name', node.service_name)]
214+
if metadata:
215+
for k, v in metadata.items():
216+
metadata_list.append((k, v))
210217

211-
resp = await stub.Unary(req, metadata=metadata)
218+
resp = await stub.Unary(req, metadata=metadata_list)
212219

213220
obj = json.loads(resp.payload.decode('utf-8'))
214221
return obj

0 commit comments

Comments
 (0)