kafka-ts 1.1.0 → 1.1.2-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cluster.js CHANGED
@@ -50,6 +50,9 @@ class Cluster {
50
50
  return this.brokerById[nodeId].sendRequest(...args);
51
51
  };
52
52
  async acquireBroker(nodeId) {
53
+ if (!(nodeId in this.brokerMetadata)) {
54
+ throw new error_1.KafkaTSError(`Broker ${nodeId} is not available`);
55
+ }
53
56
  const broker = new broker_1.Broker({
54
57
  clientId: this.options.clientId,
55
58
  sasl: this.options.sasl,
@@ -13,6 +13,7 @@ export declare class Producer {
13
13
  private producerEpoch;
14
14
  private sequences;
15
15
  private partition;
16
+ private lock;
16
17
  constructor(cluster: Cluster, options: ProducerOptions);
17
18
  send(messages: Message[], { acks }?: {
18
19
  acks?: -1 | 1;
@@ -16,6 +16,9 @@ const partitioner_1 = require("../distributors/partitioner");
16
16
  const metadata_1 = require("../metadata");
17
17
  const delay_1 = require("../utils/delay");
18
18
  const error_1 = require("../utils/error");
19
+ const lock_1 = require("../utils/lock");
20
+ const logger_1 = require("../utils/logger");
21
+ const shared_1 = require("../utils/shared");
19
22
  const tracer_1 = require("../utils/tracer");
20
23
  const trace = (0, tracer_1.createTracer)('Producer');
21
24
  class Producer {
@@ -26,6 +29,7 @@ class Producer {
26
29
  producerEpoch = 0;
27
30
  sequences = {};
28
31
  partition;
32
+ lock = new lock_1.Lock();
29
33
  constructor(cluster, options) {
30
34
  this.cluster = cluster;
31
35
  this.options = {
@@ -49,54 +53,57 @@ class Producer {
49
53
  const nodeTopicPartitionMessages = (0, messages_to_topic_partition_leaders_1.distributeMessagesToTopicPartitionLeaders)(partitionedMessages, this.metadata.getTopicPartitionLeaderIds());
50
54
  try {
51
55
  await Promise.all(Object.entries(nodeTopicPartitionMessages).map(async ([nodeId, topicPartitionMessages]) => {
52
- const topicData = Object.entries(topicPartitionMessages).map(([topic, partitionMessages]) => ({
53
- name: topic,
54
- partitionData: Object.entries(partitionMessages).map(([partition, messages]) => {
55
- const partitionIndex = parseInt(partition);
56
- let baseTimestamp;
57
- let maxTimestamp;
58
- messages.forEach(({ timestamp = defaultTimestamp }) => {
59
- if (!baseTimestamp || timestamp < baseTimestamp) {
60
- baseTimestamp = timestamp;
61
- }
62
- if (!maxTimestamp || timestamp > maxTimestamp) {
63
- maxTimestamp = timestamp;
64
- }
65
- });
66
- return {
67
- index: partitionIndex,
68
- baseOffset: 0n,
69
- partitionLeaderEpoch: -1,
70
- attributes: 0,
71
- lastOffsetDelta: messages.length - 1,
72
- baseTimestamp: baseTimestamp ?? 0n,
73
- maxTimestamp: maxTimestamp ?? 0n,
74
- producerId: this.producerId,
75
- producerEpoch: 0,
76
- baseSequence: this.getSequence(topic, partitionIndex),
77
- records: messages.map((message, index) => ({
56
+ const lockKeys = Object.entries(topicPartitionMessages).flatMap(([topic, partitionMessages]) => Object.entries(partitionMessages).map(([partition]) => `${topic}-${partition}`));
57
+ await this.lock.acquire(lockKeys, async () => {
58
+ const topicData = Object.entries(topicPartitionMessages).map(([topic, partitionMessages]) => ({
59
+ name: topic,
60
+ partitionData: Object.entries(partitionMessages).map(([partition, messages]) => {
61
+ const partitionIndex = parseInt(partition);
62
+ let baseTimestamp;
63
+ let maxTimestamp;
64
+ messages.forEach(({ timestamp = defaultTimestamp }) => {
65
+ if (!baseTimestamp || timestamp < baseTimestamp) {
66
+ baseTimestamp = timestamp;
67
+ }
68
+ if (!maxTimestamp || timestamp > maxTimestamp) {
69
+ maxTimestamp = timestamp;
70
+ }
71
+ });
72
+ return {
73
+ index: partitionIndex,
74
+ baseOffset: 0n,
75
+ partitionLeaderEpoch: -1,
78
76
  attributes: 0,
79
- timestampDelta: (message.timestamp ?? defaultTimestamp) - (baseTimestamp ?? 0n),
80
- offsetDelta: index,
81
- key: message.key ?? null,
82
- value: message.value,
83
- headers: Object.entries(message.headers ?? {}).map(([key, value]) => ({
84
- key,
85
- value,
77
+ lastOffsetDelta: messages.length - 1,
78
+ baseTimestamp: baseTimestamp ?? 0n,
79
+ maxTimestamp: maxTimestamp ?? 0n,
80
+ producerId: this.producerId,
81
+ producerEpoch: 0,
82
+ baseSequence: this.getSequence(topic, partitionIndex),
83
+ records: messages.map((message, index) => ({
84
+ attributes: 0,
85
+ timestampDelta: (message.timestamp ?? defaultTimestamp) - (baseTimestamp ?? 0n),
86
+ offsetDelta: index,
87
+ key: message.key ?? null,
88
+ value: message.value,
89
+ headers: Object.entries(message.headers ?? {}).map(([key, value]) => ({
90
+ key,
91
+ value,
92
+ })),
86
93
  })),
87
- })),
88
- };
89
- }),
90
- }));
91
- await this.cluster.sendRequestToNode(parseInt(nodeId))(api_1.API.PRODUCE, {
92
- transactionalId: null,
93
- acks,
94
- timeoutMs: 5000,
95
- topicData,
96
- });
97
- topicData.forEach(({ name, partitionData }) => {
98
- partitionData.forEach(({ index, records }) => {
99
- this.updateSequence(name, index, records.length);
94
+ };
95
+ }),
96
+ }));
97
+ await this.cluster.sendRequestToNode(parseInt(nodeId))(api_1.API.PRODUCE, {
98
+ transactionalId: null,
99
+ acks,
100
+ timeoutMs: 5000,
101
+ topicData,
102
+ });
103
+ topicData.forEach(({ name, partitionData }) => {
104
+ partitionData.forEach(({ index, records }) => {
105
+ this.updateSequence(name, index, records.length);
106
+ });
100
107
  });
101
108
  });
102
109
  }));
@@ -108,18 +115,26 @@ class Producer {
108
115
  if (error instanceof error_1.KafkaTSApiError && error.errorCode === api_1.API_ERROR.OUT_OF_ORDER_SEQUENCE_NUMBER) {
109
116
  await this.initProducerId();
110
117
  }
118
+ logger_1.log.warn('Reconnecting producer due to an unhandled error', { error });
119
+ try {
120
+ await this.cluster.disconnect();
121
+ await this.cluster.connect();
122
+ }
123
+ catch (error) {
124
+ logger_1.log.warn('Failed to reconnect producer', { error });
125
+ }
111
126
  throw error;
112
127
  }
113
128
  }
114
129
  async close() {
115
130
  await this.cluster.disconnect();
116
131
  }
117
- async ensureConnected() {
132
+ ensureConnected = (0, shared_1.shared)(async () => {
118
133
  await this.cluster.ensureConnected();
119
134
  if (!this.producerId) {
120
135
  await this.initProducerId();
121
136
  }
122
- }
137
+ });
123
138
  async initProducerId() {
124
139
  try {
125
140
  const result = await this.cluster.sendRequest(api_1.API.INIT_PRODUCER_ID, {
@@ -0,0 +1,9 @@
1
+ /// <reference types="node" />
2
+ import EventEmitter from 'events';
3
+ export declare class Lock extends EventEmitter {
4
+ private locks;
5
+ constructor();
6
+ acquire(keys: string[], callback: () => Promise<void>): Promise<void>;
7
+ private acquireKey;
8
+ private releaseKey;
9
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Lock = void 0;
7
+ const events_1 = __importDefault(require("events"));
8
+ const logger_1 = require("./logger");
9
+ class Lock extends events_1.default {
10
+ locks = {};
11
+ constructor() {
12
+ super();
13
+ this.setMaxListeners(Infinity);
14
+ }
15
+ async acquire(keys, callback) {
16
+ await Promise.all(keys.map((key) => this.acquireKey(key)));
17
+ try {
18
+ await callback();
19
+ }
20
+ finally {
21
+ keys.forEach((key) => this.releaseKey(key));
22
+ }
23
+ }
24
+ async acquireKey(key) {
25
+ while (this.locks[key]) {
26
+ await new Promise((resolve) => {
27
+ const timeout = setTimeout(() => {
28
+ logger_1.log.warn(`Lock timed out`, { key });
29
+ this.emit(`release:${key}`);
30
+ }, 10_000);
31
+ this.once(`release:${key}`, () => {
32
+ clearTimeout(timeout);
33
+ resolve();
34
+ });
35
+ });
36
+ }
37
+ this.locks[key] = true;
38
+ }
39
+ releaseKey(key) {
40
+ this.locks[key] = false;
41
+ this.emit(`release:${key}`);
42
+ }
43
+ }
44
+ exports.Lock = Lock;
@@ -0,0 +1 @@
1
+ export declare const shared: <F extends () => Promise<any>>(func: F) => () => ReturnType<F>;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shared = void 0;
4
+ const shared = (func) => {
5
+ let promise;
6
+ return () => {
7
+ if (!promise) {
8
+ promise = func();
9
+ promise.finally(() => {
10
+ promise = undefined;
11
+ });
12
+ }
13
+ return promise;
14
+ };
15
+ };
16
+ exports.shared = shared;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kafka-ts",
3
- "version": "1.1.0",
3
+ "version": "1.1.2-beta.0",
4
4
  "main": "dist/index.js",
5
5
  "author": "Priit Käärd",
6
6
  "license": "MIT",