UNPKG

@nivinjoseph/n-eda

Version:

Event Driven Architecture framework

213 lines (181 loc) 9.15 kB
import { given } from "@nivinjoseph/n-defensive"; import { ApplicationException, Exception, InvalidOperationException, ObjectDisposedException } from "@nivinjoseph/n-exception"; import { Logger } from "@nivinjoseph/n-log"; import { Delay, DelayCanceller, Disposable, Observable, Observer } from "@nivinjoseph/n-util"; import { EdaManager } from "../eda-manager.js"; // import { ConsumerTracer } from "../event-handler-tracer"; import * as otelApi from "@opentelemetry/api"; import * as semCon from "@opentelemetry/semantic-conventions"; import { WorkItem } from "./scheduler.js"; export abstract class Processor implements Disposable { private readonly _manager: EdaManager; // private readonly _consumerTracer: ConsumerTracer | null; // private readonly _hasConsumerTracer: boolean; private readonly _logger: Logger; private readonly _availabilityObserver = new Observer<this>("available"); private readonly _doneProcessingObserver = new Observer<WorkItem>("done-processing"); private _currentWorkItem: WorkItem | null = null; private _processPromise: Promise<void> | null = null; private _isDisposed = false; private _delayCanceller: DelayCanceller | null = null; private get _isInitialized(): boolean { return this._availabilityObserver.hasSubscriptions && this._doneProcessingObserver.hasSubscriptions; } protected get manager(): EdaManager { return this._manager; } protected get logger(): Logger { return this._logger; } public get availability(): Observable<this> { return this._availabilityObserver; } public get doneProcessing(): Observable<WorkItem> { return this._doneProcessingObserver; } public get isBusy(): boolean { return this._currentWorkItem != null; } public constructor(manager: EdaManager) { given(manager, "manager").ensureHasValue().ensureIsObject().ensureIsType(EdaManager); this._manager = manager; // this._consumerTracer = this._manager.consumerTracer; // this._hasConsumerTracer = this._consumerTracer != null; this._logger = this._manager.serviceLocator.resolve<Logger>("Logger"); } public process(workItem: WorkItem): void { if (!this._isInitialized || this.isBusy) throw new InvalidOperationException("processor not initialized or processor is busy"); if (this._isDisposed) { workItem.deferred.reject(new ObjectDisposedException("Processor")); return; } this._currentWorkItem = workItem; this._processPromise = this._process() .then(() => { const doneWorkItem = this._currentWorkItem!; this._doneProcessingObserver.notify(doneWorkItem); this._currentWorkItem = null; if (!this._isDisposed) this._availabilityObserver.notify(this); }) .catch((e) => this._logger.logError(e)); } public dispose(): Promise<void> { if (!this._isDisposed) { this._isDisposed = true; // console.warn("Disposing processor"); } if (this._delayCanceller) this._delayCanceller.cancel!(); return this._processPromise?.then(() => { // console.warn("Processor disposed"); }) || Promise.resolve().then(() => { // console.warn("Processor disposed"); }); } protected abstract processEvent(workItem: WorkItem): Promise<void>; private async _process(): Promise<void> { const workItem = this._currentWorkItem!; const parentContext = otelApi.trace.setSpan(otelApi.context.active(), workItem.span); const tracer = otelApi.trace.getTracer("n-eda"); const span = tracer.startSpan(`event.${workItem.event.name} process`, { kind: otelApi.SpanKind.CONSUMER, attributes: { [semCon.SemanticAttributes.MESSAGING_SYSTEM]: "n-eda", [semCon.SemanticAttributes.MESSAGING_OPERATION]: "process", [semCon.SemanticAttributes.MESSAGING_DESTINATION]: `${workItem.topic}+++${workItem.partition}`, [semCon.SemanticAttributes.MESSAGING_DESTINATION_KIND]: "topic", [semCon.SemanticAttributes.MESSAGING_TEMP_DESTINATION]: false, [semCon.SemanticAttributes.MESSAGING_PROTOCOL]: "NEDA", [semCon.SemanticAttributes.MESSAGE_ID]: workItem.event.id, [semCon.SemanticAttributes.MESSAGING_CONVERSATION_ID]: workItem.event.partitionKey } }, parentContext); // otelApi.trace.setSpan(otelApi.context.active(), span); await otelApi.context.with(otelApi.trace.setSpan(otelApi.context.active(), span), async () => { const maxProcessAttempts = 10; let numProcessAttempts = 0; try { while (numProcessAttempts < maxProcessAttempts) { if (this._isDisposed) { workItem.deferred.reject(new ObjectDisposedException("Processor")); return; } numProcessAttempts++; try { // await this._logger.logInfo(`Processing event ${workItem.eventName} with id ${workItem.eventId}`); // if (this._hasConsumerTracer) // await this._consumerTracer!({ // topic: workItem.topic, // partition: workItem.partition, // partitionKey: workItem.partitionKey, // eventName: workItem.eventName, // eventId: workItem.eventId // }, () => this.processEvent(workItem)); // else // await this.processEvent(workItem); await this.processEvent(workItem); workItem.deferred.resolve(); return; } catch (error: any) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (this._isDisposed) { workItem.deferred.reject(new ObjectDisposedException("Processor")); return; } // if (numProcessAttempts > 8) // { // await this.logger.logWarning(`Error in EventHandler while handling event of type '${workItem.eventName}' (ATTEMPT = ${numProcessAttempts}) with data ${JSON.stringify(workItem.event.serialize())}.`); // await this.logger.logWarning(error as Exception); // } await this.logger.logWarning(`Error in EventHandler while handling event of type '${workItem.eventName}' (ATTEMPT = ${numProcessAttempts}) with data ${JSON.stringify(workItem.event.serialize())}.`); await this.logger.logWarning(error); if (numProcessAttempts >= maxProcessAttempts) throw error; else { span.recordException(error as Error); const seconds = (5 + numProcessAttempts) * numProcessAttempts; // [6, 14, 24, 36, 50, 66, 84, 104, 126] span.addEvent("Waiting before retry", { "delay": `${seconds}s`, "attempt": numProcessAttempts }); this._delayCanceller = {}; await Delay.seconds(seconds, this._delayCanceller); this._delayCanceller = null; } } } } catch (error: any) { span.recordException(error as Error); span.addEvent(`Failed to process event of type '${workItem.eventName}'`, { eventData: JSON.stringify(workItem.event.serialize()) }); const message = `Failed to process event of type '${workItem.eventName}' with data ${JSON.stringify(workItem.event.serialize())}`; span.setStatus({ code: otelApi.SpanStatusCode.ERROR, message }); await this._logger.logError(message); await this._logger.logError(error); workItem.deferred.reject(new ApplicationException(message, error as Exception)); } finally { span.end(); } }); } }