UNPKG

@mwcp/kmore

Version:

midway component for knex, supports declarative transaction and OpenTelemetry

592 lines 24.3 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; import assert from 'node:assert'; import { ApplicationContext, Inject, Singleton } from '@midwayjs/core'; import { AttrNames, TraceService } from '@mwcp/otel'; import { genISO8601String } from '@waiting/shared-core'; import { PropagationType, QueryBuilderExtKey, TrxControl, genKmoreTrxId, } from 'kmore'; import { CallerService } from './caller.service.js'; import { genCallerKey, linkBuilderWithTrx } from './propagation/trx-status.helper.js'; import { // TraceEndOptions, TransactionalEntryType, } from './propagation/trx-status.types.js'; import { ConfigKey, Msg } from './types.js'; /** * Declarative transaction status manager */ let TrxStatusService = class TrxStatusService { applicationContext; appDir; // @Inject() readonly logger: TraceLogger traceSvc; callerSvc; scope2TraceContextMap = new WeakMap(); dbInstanceList = new Map(); callerKeyPropagationMapIndex = new Map(); entryCallerKeyTrxMapIndex = new Map(); getName() { return 'trxStatusService'; } // #region dbInstance registerDbInstance(dbId, db) { this.dbInstanceList.set(dbId, db); } /** * If dbId is undefined or empty * - return the only on instance * - throw error if multiple instance exists */ getDbInstance(dbId) { if (dbId) { return this.dbInstanceList.get(dbId); } if (this.dbInstanceList.size === 1) { const iterator = this.dbInstanceList.values(); const firstValue = iterator.next().value; return firstValue; } throw new Error('getDbInstance(): dbId is undefined, but multiple instances exists'); } getDbInstanceCount() { return this.dbInstanceList.size; } listDbInstanceNames() { return Array.from(this.dbInstanceList.keys()); } unregisterDbInstance(dbId) { this.dbInstanceList.delete(dbId); } // #region registerPropagation() registerPropagation(options) { const event = { event: AttrNames.TransactionalRegister, time: genISO8601String(), [AttrNames.TrxPropagationReadRowLockLevel]: options.readRowLockLevel, [AttrNames.TrxPropagationWriteRowLockLevel]: options.writeRowLockLevel, }; const { scope } = options; assert(scope, 'scope required'); const dbInstance = this.getDbInstance(options.dbSourceName); assert(dbInstance, `dbSourceName "${options.dbSourceName}" not found`); const dbSourceName = dbInstance.dbId; options.dbSourceName = dbSourceName; const key = genCallerKey(options.className, options.funcName); const tkey = this.retrieveRegisteredTopCallerKeyFromCallStack(dbSourceName, scope); const type = this.getPropagationType(dbSourceName, scope, key); if (type) { assert(type === options.type, `callerKey "${key}" has registered propagation "${type}", but want to register different "${options.type}"`); if (tkey) { // has ancestor caller that registered this.callerSvc.updateCallerTreeMap(dbSourceName, scope, tkey, key); } else { // getCallerStack will return insufficient sites if calling self without "await", so not top level caller const prefix = `[@mwcp/${ConfigKey.namespace}] registerPropagation() error: `; const msg = prefix + `${Msg.insufficientCallstacks}. Maybe calling async function without "await", Result of Query Builder MUST be "await"ed within Transactional decorator method. callerKey: "${key}\n" `; console.error(msg); const err = new Error(msg); this.traceSvc.setRootSpanWithError(err); throw err; } event[AttrNames.TransactionalEntryType] = TransactionalEntryType.sub; } else { this.setPropagationOptions(key, options); if (tkey) { // has ancestor caller that registered this.callerSvc.updateCallerTreeMap(dbSourceName, scope, tkey, key); event[AttrNames.TransactionalEntryType] = TransactionalEntryType.sub; } else { // top level caller this.callerSvc.updateCallerTreeMap(dbSourceName, scope, key, void 0); event[AttrNames.TransactionalEntryType] = TransactionalEntryType.top; } } return key; } retrieveUniqueTopCallerKey(sourceName, scope, key) { const dbInstance = this.getDbInstance(sourceName); assert(dbInstance, `dbSourceName "${sourceName}" not found`); const dbSourceName = dbInstance.dbId; assert(dbSourceName, 'dbSourceName is undefined'); return this.callerSvc.retrieveUniqueTopCallerKey(dbSourceName, scope, key); } /** * Is decorator `Transactional()` registered with current scope and callerKey */ isRegistered(dbSourceName, scope, key) { const sourceNameMap = this.callerKeyPropagationMapIndex.get(scope); if (!sourceNameMap?.size) { return false; } const callerKeyPropagationMap = sourceNameMap.get(dbSourceName); if (!callerKeyPropagationMap?.size) { return false; } return callerKeyPropagationMap.has(key); } // retrieveUpLatestCallerKey(options: DecoratorExecutorOptions): CallerKey | undefined { // const key = genCallerKey(options.className, options.funcName) // } // #region startNewTrx() async startNewTrx(options) { const { db, scope, kmoreTrxId: trxId, trxPropagateOptions } = options; const callerKey = trxPropagateOptions.key; const kmoreTrxId = trxId ?? genKmoreTrxId('trx-', callerKey); assert(kmoreTrxId, `kmoreTrxId is undefined for callerKey "${callerKey}"`); const dbSourceName = db.dbId; assert(dbSourceName, 'dbSourceName is undefined on db'); const config = { trxActionOnError: TrxControl.Rollback, ...options, kmoreTrxId, }; const trx = await db.transaction(config); assert(trx.kmoreTrxId === kmoreTrxId, `trx.kmoreTrxId "${trx.kmoreTrxId.toString()}" not equal to kmoreTrxId "${kmoreTrxId.toString()}"`); if (!trx.trxPropagateOptions) { Object.defineProperty(trx, QueryBuilderExtKey.trxPropagateOptions, { value: trxPropagateOptions, }); } const { entryKey } = options.trxPropagateOptions; try { this.updateEntryCallerKeyTrxMap(dbSourceName, scope, entryKey, trx); } catch (ex) { await trx.rollback(); throw ex; } return trx; } // #region Trx Commit /** * Only top caller can commit */ async tryCommitTrxIfKeyIsEntryTop(sourceName, scope, callerKey) { const dbInstance = this.getDbInstance(sourceName); assert(dbInstance, `dbSourceName "${sourceName}" not found`); const dbSourceName = dbInstance.dbId; assert(callerKey, 'tryCommitTrxIfKeyIsEntryTop(): callerKey is undefined'); const tkey = this.retrieveUniqueTopCallerKey(dbSourceName, scope, callerKey); if (!tkey) { // multiple callings this.callerSvc.removeLastKeyFromCallerTreeArray(dbSourceName, scope, callerKey); return; } if (tkey !== callerKey) { return; } // if (! tkey) { // const tkeyArr = this.retrieveTopCallerKeyArrayByCallerKey(scope, callerKey) // if (! tkeyArr.length) { // const msg = `${Msg.callerKeyNotRegisteredOrNotEntry}: "${callerKey}". // Maybe calling async function without "await", or has been removed with former error.\n` // // @FIXME // // this.logger.error(msg) // throw new Error(msg) // // return // } // else if (tkeyArr.length > 1) { // multiple callings // this.removeLastKeyFromCallerTreeArray(scope, callerKey) // return // } // tkey = tkeyArr[0] // assert(tkey, 'tkey is undefined') // if (tkey !== callerKey) { return } // } // if (callerKey !== tkey) { // this.removeLastKeyFromCallerTreeArray(scope, callerKey) // return // } const trxs = this.getTrxArrayByEntryKey(dbSourceName, scope, tkey); if (!trxs?.length) { this.cleanAfterTrx(void 0, scope, tkey); return; } // Delay for commit, prevent from method returning Promise or calling Knex builder without `await`! // await sleep(0) for (let i = trxs.length - 1; i >= 0; i -= 1) { const trx = trxs[i]; assert(trx, 'trx is undefined when tryCommitTrxIfKeyIsEntryTop()'); assert(trx.dbId === dbSourceName, `trx.dbId "${trx.dbId}" not equal to dbSourceName2 "${dbSourceName}"`); await trx.commit(); this.removeTrxFromEntryCallerKeyTrxMap(dbSourceName, scope, trx.kmoreTrxId); this.cleanAfterTrx(dbSourceName, scope, tkey); } } // #region Trx Rollback async trxRollbackEntry(sourceName, scope, callerKey) { const dbInstance = this.getDbInstance(sourceName); assert(dbInstance, `dbSourceName "${sourceName}" not found`); const dbSourceName = dbInstance.dbId; assert(dbSourceName, 'dbSourceName is undefined'); const tkeyArr = this.callerSvc.retrieveTopCallerKeyArrayByCallerKey(dbSourceName, scope, callerKey); let tkey = callerKey; if (tkeyArr.length > 0) { const key = tkeyArr.at(-1); // pick last one if (key) { tkey = key; } } const trxs = this.getTrxArrayByEntryKey(dbSourceName, scope, tkey); if (!trxs?.length) { this.cleanAfterTrx(void 0, scope, tkey); return; } for (const trx of trxs) { try { await trx.rollback(); } catch (ex) { const msg = `ROLLBACK failed for key: "${tkey}". This error will be ignored, continue next trx rollback.`; console.warn(msg, ex); // @FIXME // this.logger.error(msg, ex) } this.cleanAfterTrx(dbSourceName, scope, tkey); } } // #region Propagation bindBuilderPropagationData(dbSourceName, builder, distance = 0) { if (builder.trxPropagated) { return; } const { scope } = builder; assert(scope, 'scope is undefined'); const count = this.getPropagationOptionsCount(dbSourceName, scope); if (!count) { return; } let callerInfo; try { callerInfo = this.callerSvc.retrieveCallerInfo(distance + 1); if (!callerInfo.className || !callerInfo.funcName) { console.warn('Warn [@mwcp/kmore] retrieveCallerInfo() failed' + JSON.stringify(callerInfo)); return; } } catch (ex) { console.warn('[@mwcp/kmore] retrieveCallerInfo failed', ex); return; } const key = genCallerKey(callerInfo.className, callerInfo.funcName); builder.callerKey = key; const isRegistered = this.isRegistered(dbSourceName, scope, key); if (!isRegistered) { return; } const propagatingOptions = this.getPropagationOptions(dbSourceName, scope, key); if (!propagatingOptions?.type) { return; } const { readRowLockLevel, writeRowLockLevel } = propagatingOptions; this.callerSvc.validateCallerKeyUnique(dbSourceName, scope, key, callerInfo.path); this.callerSvc.setFilepathToCallerKeyFileMapIndex(dbSourceName, scope, key, callerInfo.path); // const entryKey = this.retrieveFirstAncestorCallerKeyByCallerKey(key) ?? '' const arr = this.callerSvc.retrieveTopCallerKeyArrayByCallerKey(dbSourceName, scope, key); const entryKey = arr.at(-1) ?? ''; assert(entryKey, 'entryKey is undefined'); const value = { entryKey, key, dbId: builder.dbId, type: propagatingOptions.type, path: callerInfo.path, className: callerInfo.className, funcName: callerInfo.funcName, methodName: callerInfo.methodName, line: callerInfo.line, column: callerInfo.column, readRowLockLevel, writeRowLockLevel, scope, }; Object.freeze(value); void Object.defineProperty(builder, QueryBuilderExtKey.trxPropagateOptions, { value, }); } async propagating(options) { const { db, builder } = options; const { scope } = builder; assert(scope, 'propagating(): scope is undefined'); const ret = { kmoreTrxId: void 0, }; const count = this.getPropagationOptionsCount(db.dbId, scope); if (!count) { return ret; } const { trxPropagateOptions, trxPropagated, callerKey } = builder; if (trxPropagated) { return ret; } assert(callerKey, 'propagating(): callerKey is undefined'); const propagatingOptions = this.getPropagationOptions(db.dbId, scope, callerKey); if (!propagatingOptions?.type) { return ret; } assert(trxPropagateOptions, 'propagating(): trxPropagateOptions is undefined'); switch (trxPropagateOptions.type) { case PropagationType.REQUIRED: { const trx = await this._propagatingRequired(options, trxPropagateOptions); db.linkQueryIdToTrxId(builder.kmoreQueryId, trx.kmoreTrxId); this.builderLinkTrx(options, trx); ret.kmoreTrxId = trx.kmoreTrxId; break; } case PropagationType.SUPPORTS: { const trx = await this._propagatingSupports(options, trxPropagateOptions); if (trx) { db.linkQueryIdToTrxId(builder.kmoreQueryId, trx.kmoreTrxId); } this.builderLinkTrx(options, trx); ret.kmoreTrxId = trx?.kmoreTrxId; break; } default: throw new Error(`Not implemented propagation type "${trxPropagateOptions.type}"`); } return ret; } async _propagatingRequired(options, trxPropagateOptions) { const { db, builder } = options; const { scope } = builder; assert(scope, 'scope is undefined'); const dbSourceName = db.dbId; assert(scope === trxPropagateOptions.scope, 'scope !== trxPropagateOptions.scope'); let trx = this.getCurrentTrx(dbSourceName, scope, trxPropagateOptions.entryKey); if (!trx) { trx = await this.startNewTrx({ scope, db, trxPropagateOptions, }); } assert(trx, 'trx is undefined'); return trx; } // @Trace<TrxStatusService['_propagatingRequired']>({ // scope: ([options, trxPropagateOptions]: [PropagatingOptions, TrxPropagateOptions]) => { // }, // }) async _propagatingSupports(options, trxPropagateOptions) { const { db, builder } = options; const { scope } = builder; assert(scope, 'scope is undefined'); const dbSourceName = db.dbId; const key = genCallerKey(trxPropagateOptions.className, trxPropagateOptions.funcName); const trx = this.getCurrentTrx(dbSourceName, scope, key); if (!trx) { return; } const trxPropagated = !!trx.trxPropagateOptions; if (!trxPropagated) { Object.defineProperty(trx, QueryBuilderExtKey.trxPropagateOptions, { value: trxPropagateOptions, }); } return trx; } // #region clean cleanAfterRequestFinished(scope) { this.callerSvc.deleteCallerKeyFileMapIndex(scope); this.callerKeyPropagationMapIndex.delete(scope); this.removeEntryCallerKeyTrxMap(scope); this.callerSvc.deleteCallerTreeMapIndex(scope); } cleanAfterTrx(dbSourceName, scope, callerKey) { this.entryCallerKeyTrxMapIndex.delete(scope); this.callerSvc.deleteCallerTreeMapIndex(scope); if (dbSourceName) { this.delPropagationOptions(dbSourceName, scope, callerKey); this.callerSvc.delFilepathFromCallerKeyFileMapIndex(dbSourceName, scope, callerKey); // @FIXME // this.removeTrxFromEntryCallerKeyTrxMap(dbSourceName, scope, callerKey) } } // #region builder builderLinkTrx(options, trx) { trx && linkBuilderWithTrx(options.builder, trx); } // #region entryCallerKeyTrxMapIndex getTrxArrayByEntryKey(dbSourceName, scope, key) { const sourceNameMap = this.entryCallerKeyTrxMapIndex.get(scope); if (!sourceNameMap?.size) { return; } return sourceNameMap.get(dbSourceName)?.get(key); } getCurrentTrxByEntryKey(dbSourceName, scope, key) { const sourceNameMap = this.entryCallerKeyTrxMapIndex.get(scope); if (!sourceNameMap?.size) { return; } const trxArr = sourceNameMap.get(dbSourceName)?.get(key); if (!trxArr?.length) { return; } for (const trx of trxArr) { if (!trx.isCompleted()) { return trx; } } } updateEntryCallerKeyTrxMap(dbSourceName, scope, key, trx) { let sourceNameMap = this.entryCallerKeyTrxMapIndex.get(scope); if (!sourceNameMap) { sourceNameMap = new Map(); this.entryCallerKeyTrxMapIndex.set(scope, sourceNameMap); } let callerKeyTrxArrayMap = sourceNameMap.get(dbSourceName); if (!callerKeyTrxArrayMap) { callerKeyTrxArrayMap = new Map(); sourceNameMap.set(dbSourceName, callerKeyTrxArrayMap); } let trxArr = callerKeyTrxArrayMap.get(key); if (!trxArr) { trxArr = []; callerKeyTrxArrayMap.set(key, trxArr); } trxArr.push(trx); } removeEntryCallerKeyTrxMap(scope) { this.entryCallerKeyTrxMapIndex.delete(scope); } cleanEntryCallerKeyTrxMapByKey(dbSourceName, scope, key) { const sourceNameMap = this.entryCallerKeyTrxMapIndex.get(scope); if (!sourceNameMap?.size) { return; } const callerKeyTrxArrayMap = sourceNameMap.get(dbSourceName); if (!callerKeyTrxArrayMap?.size) { return; } callerKeyTrxArrayMap.delete(key); } removeTrxFromEntryCallerKeyTrxMap(dbSourceName, scope, trxId) { const sourceNameMap = this.entryCallerKeyTrxMapIndex.get(scope); if (!sourceNameMap?.size) { return; } const callerKeyTrxArrayMap = sourceNameMap.get(dbSourceName); if (!callerKeyTrxArrayMap?.size) { return; } for (const trxArr of callerKeyTrxArrayMap.values()) { const pos = trxArr.findIndex(trx => trx.kmoreTrxId === trxId); if (pos === -1) { continue; } trxArr.splice(pos, 1); } } // #region dbIdTrxIdMapIndex getCurrentTrxId(dbSourceName, scope, callerKey) { const trx = this.getCurrentTrx(dbSourceName, scope, callerKey); return trx?.kmoreTrxId; } getCurrentTrx(dbSourceName, scope, callerKey) { const trx = this.getCurrentTrxByEntryKey(dbSourceName, scope, callerKey); if (trx) { return trx; } const entryKey = this.callerSvc.retrieveFirstAncestorCallerKeyByCallerKey(dbSourceName, scope, callerKey); if (!entryKey) { return; } const trx2 = this.getCurrentTrxByEntryKey(dbSourceName, scope, entryKey); if (trx2) { return trx2; } } // protected getCallerKeysArrayByEntryKey(entryKey: CallerKey): CallerKeyArray | undefined { // return this.callerTreeMap.get(entryKey) // } // #region callerKeyPropagationMapIndex getPropagationOptions(dbSourceName, scope, key) { const sourceNameMap = this.callerKeyPropagationMapIndex.get(scope); if (!sourceNameMap?.size) { return; } const options = sourceNameMap.get(dbSourceName)?.get(key); return options; } setPropagationOptions(key, options) { const { dbSourceName, scope } = options; assert(dbSourceName, 'dbSourceName is undefined'); assert(scope, 'scope is undefined'); let sourceNameMap = this.callerKeyPropagationMapIndex.get(scope); if (!sourceNameMap) { sourceNameMap = new Map(); this.callerKeyPropagationMapIndex.set(scope, sourceNameMap); } let callerKeyPropagationMap = sourceNameMap.get(dbSourceName); if (!callerKeyPropagationMap) { callerKeyPropagationMap = new Map(); sourceNameMap.set(dbSourceName, callerKeyPropagationMap); } callerKeyPropagationMap.set(key, options); } getPropagationOptionsCount(dbSourceName, scope) { const sourceNameMap = this.callerKeyPropagationMapIndex.get(scope); return sourceNameMap?.get(dbSourceName)?.size ?? 0; } getPropagationType(dbSourceName, scope, key) { const options = this.getPropagationOptions(dbSourceName, scope, key); return options?.type; } delPropagationOptions(dbSourceName, scope, key) { this.callerKeyPropagationMapIndex.get(scope)?.get(dbSourceName)?.delete(key); } retrieveRegisteredTopCallerKeyFromCallStack(dbSourceName, scope, limit = 128) { const callers = this.callerSvc.retrieveTopCallerKeyFromCallStack(limit); if (!callers.length) { return; } for (const key of callers) { assert(key, 'retrieveRegisteredTopCallerKeyFromCallStack() key is undefined'); if (this.isRegistered(dbSourceName, scope, key)) { return key; } } } getTraceContextByScope(scope) { const traceContextArr = this.scope2TraceContextMap.get(scope); return traceContextArr; } /** * @param scope kmoreTrxId or kmoreQueryId */ setTraceContextByScope(scope, traceContext) { this.scope2TraceContextMap.set(scope, traceContext); } removeTraceContextByScope(scope) { this.scope2TraceContextMap.delete(scope); } }; __decorate([ ApplicationContext(), __metadata("design:type", Object) ], TrxStatusService.prototype, "applicationContext", void 0); __decorate([ Inject(), __metadata("design:type", String) ], TrxStatusService.prototype, "appDir", void 0); __decorate([ Inject(), __metadata("design:type", TraceService) ], TrxStatusService.prototype, "traceSvc", void 0); __decorate([ Inject(), __metadata("design:type", CallerService) ], TrxStatusService.prototype, "callerSvc", void 0); TrxStatusService = __decorate([ Singleton() ], TrxStatusService); export { TrxStatusService }; //# sourceMappingURL=trx-status.service.js.map