Skip to content

Commit 3e5a30d

Browse files
committed
Started work on the implementation on the kafka message pattern.
1 parent 32e07c3 commit 3e5a30d

9 files changed

Lines changed: 187 additions & 24 deletions

File tree

integration/docker-compose.yml

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,40 @@ services:
4848
zookeeper:
4949
container_name: test-zookeeper
5050
hostname: zookeeper
51-
image: wurstmeister/zookeeper
51+
image: confluentinc/cp-zookeeper:5.3.0
5252
ports:
5353
- "2181:2181"
54+
environment:
55+
ZOOKEEPER_CLIENT_PORT: 2181
56+
ZOOKEEPER_TICK_TIME: 2000
5457
kafka:
5558
container_name: test-kafka
5659
hostname: kafka
57-
image: wurstmeister/kafka
60+
image: confluentinc/cp-kafka:5.3.0
61+
depends_on:
62+
- zookeeper
5863
ports:
64+
- "29092:29092"
5965
- "9092:9092"
6066
environment:
61-
KAFKA_ADVERTISED_HOST_NAME: localhost
62-
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
67+
KAFKA_BROKER_ID: 1
68+
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
69+
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
70+
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
71+
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
72+
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
73+
# schema-registry:
74+
# container_name: test-schema-registry
75+
# hostname: schema-registry
76+
# image: confluentinc/cp-schema-registry:5.3.0
77+
# depends_on:
78+
# - zookeeper
79+
# - kafka
80+
# ports:
81+
# - "8081:8081"
82+
# environment:
83+
# SCHEMA_REGISTRY_KAFKASTORE_CONNECTION_URL: zookeeper:2181
84+
# SCHEMA_REGISTRY_HOST_NAME: schema-registry
85+
# SCHEMA_REGISTRY_LISTENERS: http://localhost:8081
86+
# SCHEMA_REGISTRY_DEBUG: 'true'
87+

integration/microservices/src/kafka/kafka.controller.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ import {
55
EventPattern,
66
Transport,
77
} from '@nestjs/microservices';
8-
import { Observable } from 'rxjs';
8+
import { Logger } from '@nestjs/common/services/logger.service';
9+
import * as util from 'util';
910

11+
import { Observable } from 'rxjs';
1012
import * as uuid from 'uuid';
1113

1214
@Controller()
1315
export class KafkaController implements OnModuleInit {
16+
protected readonly logger = new Logger(KafkaController.name);
1417
static IS_NOTIFIED = false;
1518
static MATH_SUM = 0;
1619

@@ -29,8 +32,8 @@ export class KafkaController implements OnModuleInit {
2932
}
3033

3134
private parse(data: any) {
32-
data.message.key = data.message.key.toString();
33-
data.message.value = JSON.parse(data.message.value.toString());
35+
data.key = data.key.toString();
36+
data.value = JSON.parse(data.value.toString());
3437
}
3538

3639
@Post()
@@ -39,17 +42,28 @@ export class KafkaController implements OnModuleInit {
3942
@Query('command') cmd,
4043
@Body() data: number[],
4144
): Promise<Observable<any>> {
42-
return this.client.emit('math.sum', data.map((num) => {
45+
const key = uuid.v4(); // stick to a single partition
46+
47+
const result = await this.client.emit('math.sum', data.map((num) => {
4348
return {
44-
key: uuid.v4(), // stick to a single partition
49+
key,
4550
value: num.toString(),
51+
headers: {
52+
'correlation-id': key,
53+
},
4654
};
47-
}));
55+
})).toPromise();
56+
57+
this.logger.error(util.format('@Query math.sum result %o', result));
58+
59+
return result;
4860
}
4961

5062
@EventPattern('math.sum')
5163
mathSum(data: any){
52-
KafkaController.MATH_SUM += parseFloat(data.message.value);
64+
this.logger.error(util.format('@EventPattern math.sum data %o', data));
65+
66+
KafkaController.MATH_SUM += parseFloat(data.value);
5367
}
5468

5569
@Post('notify')
@@ -66,6 +80,6 @@ export class KafkaController implements OnModuleInit {
6680
eventHandler(data: any) {
6781
this.parse(data);
6882

69-
KafkaController.IS_NOTIFIED = data.message.value.notify;
83+
KafkaController.IS_NOTIFIED = data.value.notify;
7084
}
7185
}

packages/common/interfaces/external/kafka-options.interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export interface PartitionMetadata {
8282
}
8383

8484
export interface IHeaders {
85-
[key: string]: string;
85+
[key: string]: Buffer;
8686
}
8787

8888
export interface ConsumerConfig {

packages/microservices/client/client-kafka.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Logger } from '@nestjs/common/services/logger.service';
22
import { loadPackage } from '@nestjs/common/utils/load-package.util';
3-
import { EventEmitter } from 'events';
43
import { Observable } from 'rxjs';
54

65
import {
@@ -23,11 +22,7 @@ export class ClientKafka extends ClientProxy {
2322
protected readonly logger = new Logger(ClientKafka.name);
2423
private client: Kafka = null;
2524
private producer: Producer = null;
26-
protected channel: any = null;
27-
protected urls: string[];
28-
protected queue: string;
29-
protected queueOptions: any;
30-
protected responseEmitter: EventEmitter;
25+
3126

3227
private readonly brokers: string[];
3328
private readonly clientId: string;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './transport.enum';
2+
export * from './kafka-headers.enum';
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export enum KafkaHeaders {
2+
ACKNOWLEDGMENT = 'kafka_acknowledgment',
3+
BATCH_CONVERTED_HEADERS = 'kafka_batchConvertedHeaders',
4+
CONSUMER = 'kafka_consumer',
5+
CORRELATION_ID = 'kafka_correlationId',
6+
DLT_EXCEPTION_FQCN = 'kafka_dlt-exception-fqcn',
7+
DLT_EXCEPTION_MESSAGE = 'kafka_dlt-exception-message',
8+
DLT_EXCEPTION_STACKTRACE = 'kafka_dlt-exception-stacktrace',
9+
DLT_ORIGINAL_OFFSET = 'kafka_dlt-original-offset',
10+
DLT_ORIGINAL_PARTITION = 'kafka_dlt-original-partition',
11+
DLT_ORIGINAL_TIMESTAMP = 'kafka_dlt-original-timestamp',
12+
DLT_ORIGINAL_TIMESTAMP_TYPE = 'kafka_dlt-original-timestamp-type',
13+
DLT_ORIGINAL_TOPIC = 'kafka_dlt-original-topic',
14+
MESSAGE_KEY = 'kafka_messageKey',
15+
NATIVE_HEADERS = 'kafka_nativeHeaders',
16+
OFFSET = 'kafka_offset',
17+
PARTITION_ID = 'kafka_partitionId',
18+
PREFIX = 'kafka_',
19+
RAW_DATA = 'kafka_data',
20+
RECEIVED = 'kafka_received',
21+
RECEIVED_MESSAGE_KEY = 'kafka_receivedMessageKey',
22+
RECEIVED_PARTITION_ID = 'kafka_receivedPartitionId',
23+
RECEIVED_TIMESTAMP = 'kafka_receivedTimestamp',
24+
RECEIVED_TOPIC = 'kafka_receivedTopic',
25+
RECORD_METADATA = 'kafka_recordMetadata',
26+
REPLY_PARTITION = 'kafka_replyPartition',
27+
REPLY_TOPIC = 'kafka_replyTopic',
28+
TIMESTAMP = 'kafka_timestamp',
29+
TIMESTAMP_TYPE = 'kafka_timestampType',
30+
TOPIC = 'kafka_topic',
31+
}

packages/microservices/external/kafka.interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export interface PartitionMetadata {
8282
}
8383

8484
export interface IHeaders {
85-
[key: string]: string;
85+
[key: string]: Buffer;
8686
}
8787

8888
export interface ConsumerConfig {

packages/microservices/interfaces/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ export * from './microservice-configuration.interface';
77
export * from './packet.interface';
88
export * from './pattern-metadata.interface';
99
export * from './request-context.interface';
10-
export * from './pattern.interface';
10+
export * from './pattern.interface';

packages/microservices/server/server-kafka.ts

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { isUndefined } from '@nestjs/common/utils/shared.utils';
2+
import { Observable } from 'rxjs';
13
import { Logger } from '@nestjs/common/services/logger.service';
24
import {
35
KAFKA_DEFAULT_BROKER,
@@ -8,18 +10,29 @@ import {
810
KafkaConfig,
911
Kafka,
1012
Consumer,
13+
Producer,
1114
EachMessagePayload,
15+
KafkaMessage,
16+
Message,
1217
logLevel
1318
} from '../external/kafka.interface';
14-
import { CustomTransportStrategy, KafkaOptions } from '../interfaces';
19+
import { CustomTransportStrategy, KafkaOptions, ReadPacket, PacketId } from '../interfaces';
20+
import { KafkaHeaders } from '../enums';
1521
import { Server } from './server';
22+
import { partition } from 'rxjs/operators';
23+
24+
interface KafkaPacket {
25+
replyTopic?: string;
26+
replyPartition?: number;
27+
}
1628

1729
let kafkaPackage: any = {};
1830

1931
export class ServerKafka extends Server implements CustomTransportStrategy {
2032
protected readonly logger = new Logger(ServerKafka.name);
2133
private client: Kafka = null;
2234
private consumer: Consumer = null;
35+
private producer: Producer = null;
2336
private readonly brokers: string[];
2437
private readonly clientId: string;
2538
private readonly groupId: string;
@@ -40,16 +53,22 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
4053

4154
public close(): void {
4255
this.consumer && this.consumer.disconnect();
56+
this.producer && this.producer.disconnect();
4357
this.consumer = null;
58+
this.producer = null;
4459
this.client = null;
4560
}
4661

4762
public async start(callback: () => void): Promise<void> {
63+
// create consumer and producer
4864
this.consumer = this.client.consumer(Object.assign(this.options.consumer || {}, {
4965
groupId: this.groupId
5066
}));
5167

68+
this.producer = this.client.producer(this.options.producer);
69+
5270
await this.consumer.connect();
71+
await this.producer.connect();
5372
await this.bindEvents(this.consumer);
5473
callback();
5574
}
@@ -109,9 +128,87 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
109128
public async handleMessage(
110129
payload: EachMessagePayload
111130
) {
112-
return this.handleEvent(payload.topic, {
131+
const packet = this.deserialize(payload);
132+
const handler = this.getHandlerByPattern(packet.pattern);
133+
134+
if (handler.isEventHandler) {
135+
return this.handleEvent(packet.pattern, packet);
136+
}
137+
138+
// message handlers need at least a correlation id and a reply topic
139+
if (isUndefined(packet.id) || isUndefined(packet.replyTopic)) {
140+
throw new Error('Messaging not available');
141+
}
142+
143+
const response$ = this.transformToObservable(
144+
await handler(packet.data),
145+
) as Observable<any>;
146+
147+
const publish = <T>(data: T) =>
148+
this.sendMessage(
149+
data as T & Message,
150+
packet.replyTopic,
151+
packet.replyPartition,
152+
packet.id
153+
);
154+
155+
response$ && this.send(response$, publish);
156+
}
157+
158+
private deserialize(payload: EachMessagePayload): KafkaPacket & ReadPacket<Message> & PacketId {
159+
// build
160+
const packet = {
161+
id: undefined,
162+
replyTopic: undefined,
163+
replyPartition: undefined,
113164
pattern: payload.topic,
114-
data: payload
165+
data: Object.assign(payload.message, {
166+
topic: payload.topic,
167+
partition: payload.partition
168+
})
169+
};
170+
171+
// parse the correlation id
172+
if (!isUndefined(packet.data.headers[KafkaHeaders.CORRELATION_ID])) {
173+
// assign the correlation id as the packet id
174+
packet.id = packet.data.headers[KafkaHeaders.CORRELATION_ID].toString();
175+
176+
// parse the topic and partition
177+
if (!isUndefined(packet.data.headers[KafkaHeaders.REPLY_TOPIC])) {
178+
packet.replyTopic = packet.data.headers[KafkaHeaders.REPLY_TOPIC].toString();
179+
}
180+
181+
if (!isUndefined(packet.data.headers[KafkaHeaders.REPLY_PARTITION])) {
182+
packet.replyPartition = parseFloat(packet.data.headers[KafkaHeaders.REPLY_PARTITION].toString());
183+
}
184+
}
185+
186+
return packet;
187+
}
188+
189+
public sendMessage<T = any>(
190+
message: T & Message,
191+
replyTopic: string,
192+
replyPartition: number,
193+
correlationId: string
194+
): void {
195+
// assign partition
196+
message = Object.assign(message, {
197+
partition: replyPartition || undefined
115198
});
199+
200+
// create headers if they don't exist
201+
if (isUndefined(message.headers)) {
202+
message.headers = {};
203+
}
204+
205+
// assign the correlation id
206+
message.headers[KafkaHeaders.CORRELATION_ID] = Buffer.from(correlationId);
207+
208+
// send
209+
this.producer.send(Object.assign({
210+
topic: replyTopic,
211+
messages: [message]
212+
}, this.options.send || {}));
116213
}
117214
}

0 commit comments

Comments
 (0)