UNPKG

@platformatic/kafka

Version:

Modern and performant client for Apache Kafka

552 lines (551 loc) 25.4 kB
import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../../apis/callbacks.js"; import { FindCoordinatorKeyTypes } from "../../apis/enumerations.js"; import { adminClientQuotasChannel, adminGroupsChannel, adminLogDirsChannel, adminTopicsChannel, createDiagnosticContext } from "../../diagnostic.js"; import { MultipleErrors } from "../../errors.js"; import { Reader } from "../../protocol/reader.js"; import { Base, kAfterCreate, kCheckNotClosed, kGetApi, kGetBootstrapConnection, kGetConnection, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kValidateOptions } from "../base/base.js"; import { alterClientQuotasOptionsValidator, createTopicsOptionsValidator, deleteGroupsOptionsValidator, deleteTopicsOptionsValidator, describeClientQuotasOptionsValidator, describeGroupsOptionsValidator, describeLogDirsOptionsValidator, listGroupsOptionsValidator, listTopicsOptionsValidator } from "./options.js"; export class Admin extends Base { constructor(options) { super(options); this[kAfterCreate]('admin'); } listTopics(options, callback) { if (!callback) { callback = createPromisifiedCallback(); } if (this[kCheckNotClosed](callback)) { return callback[kCallbackPromise]; } if (!options) { options = {}; } const validationError = this[kValidateOptions](options, listTopicsOptionsValidator, '/options', false); if (validationError) { callback(validationError, undefined); return callback[kCallbackPromise]; } adminTopicsChannel.traceCallback(this.#listTopics, 1, createDiagnosticContext({ client: this, operation: 'listTopics', options }), this, options, callback); return callback[kCallbackPromise]; } createTopics(options, callback) { if (!callback) { callback = createPromisifiedCallback(); } if (this[kCheckNotClosed](callback)) { return callback[kCallbackPromise]; } const validationError = this[kValidateOptions](options, createTopicsOptionsValidator, '/options', false); if (validationError) { callback(validationError, undefined); return callback[kCallbackPromise]; } adminTopicsChannel.traceCallback(this.#createTopics, 1, createDiagnosticContext({ client: this, operation: 'createTopics', options }), this, options, callback); return callback[kCallbackPromise]; } deleteTopics(options, callback) { if (!callback) { callback = createPromisifiedCallback(); } if (this[kCheckNotClosed](callback)) { return callback[kCallbackPromise]; } const validationError = this[kValidateOptions](options, deleteTopicsOptionsValidator, '/options', false); if (validationError) { callback(validationError, undefined); return callback[kCallbackPromise]; } adminTopicsChannel.traceCallback(this.#deleteTopics, 1, createDiagnosticContext({ client: this, operation: 'deleteTopics', options }), this, options, callback); return callback[kCallbackPromise]; } listGroups(options, callback) { if (!callback) { callback = createPromisifiedCallback(); } if (this[kCheckNotClosed](callback)) { return callback[kCallbackPromise]; } if (!options) { options = {}; } const validationError = this[kValidateOptions](options, listGroupsOptionsValidator, '/options', false); if (validationError) { callback(validationError, undefined); return callback[kCallbackPromise]; } options.types ??= ['classic']; adminGroupsChannel.traceCallback(this.#listGroups, 1, createDiagnosticContext({ client: this, operation: 'listGroups', options }), this, options, callback); return callback[kCallbackPromise]; } describeGroups(options, callback) { if (!callback) { callback = createPromisifiedCallback(); } if (this[kCheckNotClosed](callback)) { return callback[kCallbackPromise]; } const validationError = this[kValidateOptions](options, describeGroupsOptionsValidator, '/options', false); if (validationError) { callback(validationError, undefined); return callback[kCallbackPromise]; } adminGroupsChannel.traceCallback(this.#describeGroups, 1, createDiagnosticContext({ client: this, operation: 'describeGroups', options }), this, options, callback); return callback[kCallbackPromise]; } deleteGroups(options, callback) { if (!callback) { callback = createPromisifiedCallback(); } if (this[kCheckNotClosed](callback)) { return callback[kCallbackPromise]; } const validationError = this[kValidateOptions](options, deleteGroupsOptionsValidator, '/options', false); if (validationError) { callback(validationError, undefined); return callback[kCallbackPromise]; } adminGroupsChannel.traceCallback(this.#deleteGroups, 1, createDiagnosticContext({ client: this, operation: 'deleteGroups', options }), this, options, callback); return callback[kCallbackPromise]; } describeClientQuotas(options, callback) { if (!callback) { callback = createPromisifiedCallback(); } if (this[kCheckNotClosed](callback)) { return callback[kCallbackPromise]; } const validationError = this[kValidateOptions](options, describeClientQuotasOptionsValidator, '/options', false); if (validationError) { callback(validationError, undefined); return callback[kCallbackPromise]; } adminClientQuotasChannel.traceCallback(this.#describeClientQuotas, 1, createDiagnosticContext({ client: this, operation: 'describeClientQuotas', options }), this, options, callback); return callback[kCallbackPromise]; } alterClientQuotas(options, callback) { if (!callback) { callback = createPromisifiedCallback(); } if (this[kCheckNotClosed](callback)) { return callback[kCallbackPromise]; } const validationError = this[kValidateOptions](options, alterClientQuotasOptionsValidator, '/options', false); if (validationError) { callback(validationError, undefined); return callback[kCallbackPromise]; } adminClientQuotasChannel.traceCallback(this.#alterClientQuotas, 1, createDiagnosticContext({ client: this, operation: 'alterClientQuotas', options }), this, options, callback); return callback[kCallbackPromise]; } describeLogDirs(options, callback) { if (!callback) { callback = createPromisifiedCallback(); } /* c8 ignore next 3 - Hard to test */ if (this[kCheckNotClosed](callback)) { return callback[kCallbackPromise]; } const validationError = this[kValidateOptions](options, describeLogDirsOptionsValidator, '/options', false); if (validationError) { callback(validationError, undefined); return callback[kCallbackPromise]; } adminLogDirsChannel.traceCallback(this.#describeLogDirs, 1, createDiagnosticContext({ client: this, operation: 'describeLogDirs', options }), this, options, callback); return callback[kCallbackPromise]; } #listTopics(options, callback) { const includeInternals = options.includeInternals ?? false; this[kPerformDeduplicated]('metadata', deduplicateCallback => { this[kPerformWithRetry]('metadata', retryCallback => { this[kGetBootstrapConnection]((error, connection) => { if (error) { retryCallback(error, undefined); return; } this[kGetApi]('Metadata', (error, api) => { if (error) { retryCallback(error, undefined); return; } api(connection, null, false, false, retryCallback); }); }); }, (error, metadata) => { if (error) { deduplicateCallback(error, undefined); return; } const topics = new Set(); for (const { name, isInternal } of metadata.topics) { /* c8 ignore next 3 - Sometimes internal topics might be returned by Kafka */ if (isInternal && !includeInternals) { continue; } topics.add(name); } deduplicateCallback(null, Array.from(topics).sort()); }, 0); }, callback); } #createTopics(options, callback) { const numPartitions = options.partitions ?? 1; const replicationFactor = options.replicas ?? 1; const assignments = []; const configs = options.configs ?? []; for (const { partition, brokers } of options.assignments ?? []) { assignments.push({ partitionIndex: partition, brokerIds: brokers }); } const requests = []; for (const topic of options.topics) { requests.push({ name: topic, numPartitions, replicationFactor, assignments, configs }); } this[kPerformDeduplicated]('createTopics', deduplicateCallback => { this[kPerformWithRetry]('createTopics', retryCallback => { this[kGetBootstrapConnection]((error, connection) => { if (error) { retryCallback(error, undefined); return; } this[kGetApi]('CreateTopics', (error, api) => { if (error) { retryCallback(error, undefined); return; } api(connection, requests, this[kOptions].timeout, false, retryCallback); }); }); }, (error, response) => { if (error) { deduplicateCallback(error, undefined); return; } const created = []; for (const { name, topicId: id, numPartitions: partitions, replicationFactor: replicas, configs } of response.topics) { const configuration = {}; for (const { name, value } of configs) { configuration[name] = value; } created.push({ id, name, partitions, replicas, configuration }); } deduplicateCallback(null, created); }, 0); }, callback); } #deleteTopics(options, callback) { this[kPerformDeduplicated]('deleteTopics', deduplicateCallback => { this[kPerformWithRetry]('deleteTopics', retryCallback => { this[kGetBootstrapConnection]((error, connection) => { if (error) { retryCallback(error, undefined); return; } const requests = []; for (const topic of options.topics) { requests.push({ name: topic }); } this[kGetApi]('DeleteTopics', (error, api) => { if (error) { retryCallback(error, undefined); return; } api(connection, requests, this[kOptions].timeout, retryCallback); }); }); }, deduplicateCallback, 0); }, error => callback(error)); } #listGroups(options, callback) { // Find all the brokers in the cluster this[kMetadata]({ topics: [] }, (error, metadata) => { if (error) { callback(error, undefined); return; } runConcurrentCallbacks('Listing groups failed.', metadata.brokers, ([, broker], concurrentCallback) => { this[kGetConnection](broker, (error, connection) => { if (error) { concurrentCallback(error, undefined); return; } this[kPerformWithRetry]('listGroups', retryCallback => { this[kGetApi]('ListGroups', (error, api) => { if (error) { retryCallback(error, undefined); return; } /* c8 ignore next 5 */ if (api.version === 4) { api(connection, options.states ?? [], retryCallback); } else { api(connection, options.states ?? [], options.types, retryCallback); } }); }, concurrentCallback, 0); }); }, (error, results) => { if (error) { callback(error, undefined); return; } const groups = new Map(); for (const result of results) { for (const raw of result.groups) { groups.set(raw.groupId, { id: raw.groupId, state: raw.groupState.toUpperCase(), groupType: raw.groupType, protocolType: raw.protocolType }); } } callback(null, groups); }); }); } #describeGroups(options, callback) { this[kMetadata]({ topics: [] }, (error, metadata) => { if (error) { callback(error, undefined); return; } this.#findGroupCoordinator(options.groups, (error, response) => { if (error) { callback(error, undefined); return; } // Group the groups by coordinator const coordinators = new Map(); for (const { key: group, nodeId: node } of response.coordinators) { let coordinator = coordinators.get(node); if (!coordinator) { coordinator = []; coordinators.set(node, coordinator); } coordinator.push(group); } runConcurrentCallbacks('Describing groups failed.', coordinators, ([node, groups], concurrentCallback) => { this[kGetConnection](metadata.brokers.get(node), (error, connection) => { if (error) { concurrentCallback(error, undefined); return; } this[kPerformWithRetry]('describeGroups', retryCallback => { this[kGetApi]('DescribeGroups', (error, api) => { if (error) { retryCallback(error, undefined); return; } api(connection, groups, options.includeAuthorizedOperations ?? false, retryCallback); }); }, concurrentCallback, 0); }); }, (error, results) => { if (error) { callback(error, undefined); return; } const groups = new Map(); for (const result of results) { for (const raw of result.groups) { const group = { id: raw.groupId, state: raw.groupState.toUpperCase(), protocolType: raw.protocolType, protocol: raw.protocolData, members: new Map(), authorizedOperations: raw.authorizedOperations }; for (const member of raw.members) { const reader = Reader.from(member.memberMetadata); let memberMetadata; let memberAssignments; if (reader.remaining > 0) { memberMetadata = { version: reader.readInt16(), topics: reader.readArray(r => r.readString(false), false, false), metadata: reader.readBytes(false) }; reader.reset(member.memberAssignment); reader.skip(2); // Ignore Version information memberAssignments = reader.readMap(r => { const topic = r.readString(false); return [topic, { topic, partitions: reader.readArray(r => r.readInt32(), false, false) }]; }, false, false); reader.readBytes(); // Ignore the user data } group.members.set(member.memberId, { id: member.memberId, groupInstanceId: member.groupInstanceId, clientId: member.clientId, clientHost: member.clientHost, metadata: memberMetadata, assignments: memberAssignments }); } groups.set(group.id, group); } } callback(null, groups); }); }); }); } #deleteGroups(options, callback) { this[kMetadata]({ topics: [] }, (error, metadata) => { if (error) { callback(error); return; } this.#findGroupCoordinator(options.groups, (error, response) => { if (error) { callback(error); return; } // Group the groups by coordinator const coordinators = new Map(); for (const { key: group, nodeId: node } of response.coordinators) { let coordinator = coordinators.get(node); if (!coordinator) { coordinator = []; coordinators.set(node, coordinator); } coordinator.push(group); } runConcurrentCallbacks('Deleting groups failed.', coordinators, ([node, groups], concurrentCallback) => { this[kGetConnection](metadata.brokers.get(node), (error, connection) => { if (error) { concurrentCallback(error, undefined); return; } this[kPerformWithRetry]('deleteGroups', retryCallback => { this[kGetApi]('DeleteGroups', (error, api) => { if (error) { retryCallback(error, undefined); return; } api(connection, groups, retryCallback); }); }, concurrentCallback, 0); }); }, error => callback(error)); }); }); } #findGroupCoordinator(groups, callback) { this[kPerformWithRetry]('findGroupCoordinator', retryCallback => { this[kGetBootstrapConnection]((error, connection) => { if (error) { retryCallback(error, undefined); return; } this[kGetApi]('FindCoordinator', (error, api) => { if (error) { retryCallback(error, undefined); return; } api(connection, FindCoordinatorKeyTypes.GROUP, groups, retryCallback); }); }); }, (error, response) => { if (error) { callback(error, undefined); return; } callback(null, response); }, 0); } #describeClientQuotas(options, callback) { this[kPerformWithRetry]('describeClientQuotas', retryCallback => { this[kGetBootstrapConnection]((error, connection) => { if (error) { retryCallback(error, undefined); return; } this[kGetApi]('DescribeClientQuotas', (error, api) => { if (error) { retryCallback(error, undefined); return; } api(connection, options.components, options.strict ?? false, retryCallback); }); }); }, (error, response) => { if (error) { callback(new MultipleErrors('Describing client quotas failed.', [error]), undefined); return; } callback(null, response.entries); }, 0); } #alterClientQuotas(options, callback) { this[kPerformWithRetry]('alterClientQuotas', retryCallback => { this[kGetBootstrapConnection]((error, connection) => { if (error) { retryCallback(error, undefined); return; } this[kGetApi]('AlterClientQuotas', (error, api) => { if (error) { retryCallback(error, undefined); return; } api(connection, options.entries, options.validateOnly ?? false, retryCallback); }); }); }, (error, response) => { if (error) { callback(new MultipleErrors('Altering client quotas failed.', [error]), undefined); return; } callback(null, response.entries); }, 0); } #describeLogDirs(options, callback) { this[kMetadata]({ topics: [] }, (error, metadata) => { /* c8 ignore next 4 - Hard to test */ if (error) { callback(error, undefined); return; } runConcurrentCallbacks('Describing log dirs failed.', metadata.brokers, ([id, broker], concurrentCallback) => { this[kGetConnection](broker, (error, connection) => { if (error) { concurrentCallback(error, undefined); return; } this[kPerformWithRetry]('describeLogDirs', retryCallback => { this[kGetApi]('DescribeLogDirs', (error, api) => { if (error) { retryCallback(error, undefined); return; } api(connection, options.topics, retryCallback); }); }, (error, response) => { if (error) { concurrentCallback(error, undefined); return; } concurrentCallback(null, { broker: id, throttleTimeMs: response.throttleTimeMs, results: response.results.map(result => ({ logDir: result.logDir, topics: result.topics, totalBytes: result.totalBytes, usableBytes: result.usableBytes })) }); }, 0); }); }, callback); }); } }