|
1 | 1 | 'use strict'; |
2 | 2 |
|
3 | | -const { parentPort } = require('worker_threads'); |
| 3 | +const { parentPort, threadId: workerThreadId } = require('worker_threads'); |
4 | 4 | const crypto = require('crypto'); |
5 | 5 | const logger = require('../logger'); |
6 | 6 | const { webhooks: Webhooks } = require('../webhooks'); |
@@ -43,7 +43,8 @@ const { |
43 | 43 | readEnvValue, |
44 | 44 | emitChangeEvent, |
45 | 45 | filterEmptyObjectValues, |
46 | | - resolveCredentials |
| 46 | + resolveCredentials, |
| 47 | + getDateBuckets |
47 | 48 | } = require('../tools'); |
48 | 49 |
|
49 | 50 | const { |
@@ -84,6 +85,8 @@ async function metricsMeta(meta, logger, key, method, ...args) { |
84 | 85 | } |
85 | 86 | } |
86 | 87 |
|
| 88 | +const pendingIdempotencyOperations = new Map(); |
| 89 | + |
87 | 90 | class BaseClient { |
88 | 91 | constructor(account, options) { |
89 | 92 | this.account = account; |
@@ -619,7 +622,136 @@ class BaseClient { |
619 | 622 | return oauthCredentials; |
620 | 623 | } |
621 | 624 |
|
| 625 | + async checkIdempotencyKey(objName, idempotencyKey) { |
| 626 | + if (!idempotencyKey) { |
| 627 | + return null; |
| 628 | + } |
| 629 | + |
| 630 | + const idempotencyKeyName = objName ? `${objName}/${idempotencyKey}` : idempotencyKey; |
| 631 | + |
| 632 | + // check last 24-48 hours, so probably will return 2 keys, at rare cases 1 |
| 633 | + const { bucketKeys } = getDateBuckets(1 * 24 * 3600); |
| 634 | + |
| 635 | + let idempotencyResultStr = await this.redis.eeGetIdempotency( |
| 636 | + `${REDIS_PREFIX}idempotency:bucket:`, |
| 637 | + idempotencyKeyName, |
| 638 | + this.runIndex, |
| 639 | + workerThreadId, |
| 640 | + bucketKeys.join(',') |
| 641 | + ); |
| 642 | + |
| 643 | + let idempotencyData; |
| 644 | + try { |
| 645 | + idempotencyData = JSON.parse(idempotencyResultStr); |
| 646 | + idempotencyData.idempotencyKey = idempotencyKey; |
| 647 | + idempotencyData.idempotencyKeyName = idempotencyKeyName; |
| 648 | + } catch (err) { |
| 649 | + this.logger.error({ msg: 'Failed to parse idempotency data', idempotencyKey, cachedValue: idempotencyResultStr, err }); |
| 650 | + } |
| 651 | + |
| 652 | + if (idempotencyData?.status === 'new') { |
| 653 | + if (pendingIdempotencyOperations.has(idempotencyKeyName)) { |
| 654 | + let error = new Error('Cancelling pending operation'); |
| 655 | + for (let promise of pendingIdempotencyOperations.get(idempotencyKeyName)) { |
| 656 | + promise.reject(error); |
| 657 | + } |
| 658 | + } |
| 659 | + pendingIdempotencyOperations.set(idempotencyKeyName, []); |
| 660 | + } |
| 661 | + |
| 662 | + // use existing response |
| 663 | + switch (idempotencyData.status) { |
| 664 | + case 'completed': |
| 665 | + idempotencyData.returnValue = Object.assign({}, idempotencyData.result, { |
| 666 | + idempotency: { key: idempotencyData.idempotencyKey, status: 'HIT' } |
| 667 | + }); |
| 668 | + break; |
| 669 | + case 'pending': { |
| 670 | + let queueResult = await new Promise((resolve, reject) => { |
| 671 | + pendingIdempotencyOperations.get(idempotencyData.idempotencyKeyName).push({ resolve, reject }); |
| 672 | + }); |
| 673 | + idempotencyData.returnValue = Object.assign({}, queueResult, { idempotency: { key: idempotencyData.idempotencyKey, status: 'HIT' } }); |
| 674 | + break; |
| 675 | + } |
| 676 | + } |
| 677 | + |
| 678 | + return idempotencyData || null; |
| 679 | + } |
| 680 | + |
| 681 | + async updateIdempotencyData(idempotencyData, result) { |
| 682 | + if (idempotencyData?.bucketKey && idempotencyData?.idempotencyKeyName) { |
| 683 | + // update status and result |
| 684 | + try { |
| 685 | + await this.redis.hset( |
| 686 | + idempotencyData?.bucketKey, |
| 687 | + idempotencyData.idempotencyKeyName, |
| 688 | + JSON.stringify({ |
| 689 | + status: 'completed', |
| 690 | + runIndex: idempotencyData.runIndex, |
| 691 | + threadId: idempotencyData.threadId, |
| 692 | + result |
| 693 | + }) |
| 694 | + ); |
| 695 | + } catch (err) { |
| 696 | + this.logger.error({ |
| 697 | + msg: 'Failed to update idempotency data', |
| 698 | + idempotencyKey: idempotencyData.idempotencyKey, |
| 699 | + err |
| 700 | + }); |
| 701 | + } |
| 702 | + |
| 703 | + for (let promise of pendingIdempotencyOperations.get(idempotencyData.idempotencyKeyName)) { |
| 704 | + promise.resolve(result); |
| 705 | + } |
| 706 | + } |
| 707 | + } |
| 708 | + |
| 709 | + async clearIdempotencyData(idempotencyData, error) { |
| 710 | + if (idempotencyData?.status === 'new' && idempotencyData?.bucketKey && idempotencyData?.idempotencyKeyName) { |
| 711 | + // delete failed attempt information |
| 712 | + try { |
| 713 | + await this.redis.hdel(idempotencyData?.bucketKey, idempotencyData.idempotencyKeyName); |
| 714 | + } catch (err) { |
| 715 | + this.logger.error({ |
| 716 | + msg: 'Failed to clear idempotency data', |
| 717 | + idempotencyKey: idempotencyData.idempotencyKey, |
| 718 | + err |
| 719 | + }); |
| 720 | + } |
| 721 | + |
| 722 | + for (let promise of pendingIdempotencyOperations.get(idempotencyData.idempotencyKeyName)) { |
| 723 | + promise.reject(error); |
| 724 | + } |
| 725 | + } |
| 726 | + } |
| 727 | + |
622 | 728 | async queueMessage(data, meta, connectionOptions) { |
| 729 | + let idempotencyData; |
| 730 | + |
| 731 | + if (meta.idempotencyKey) { |
| 732 | + idempotencyData = await this.checkIdempotencyKey(`mq/${this.account}`, meta.idempotencyKey); |
| 733 | + if (idempotencyData?.returnValue) { |
| 734 | + return idempotencyData?.returnValue; |
| 735 | + } |
| 736 | + } |
| 737 | + |
| 738 | + let queueResult; |
| 739 | + try { |
| 740 | + queueResult = await this.queueMessageHandler(data, meta, connectionOptions); |
| 741 | + await this.updateIdempotencyData(idempotencyData, queueResult); |
| 742 | + } catch (err) { |
| 743 | + await this.clearIdempotencyData(idempotencyData, err); |
| 744 | + throw err; |
| 745 | + } |
| 746 | + |
| 747 | + if (idempotencyData?.status === 'new') { |
| 748 | + return Object.assign({}, queueResult, { idempotency: { key: idempotencyData.idempotencyKey, status: 'MISS' } }); |
| 749 | + } |
| 750 | + |
| 751 | + return queueResult; |
| 752 | + } |
| 753 | + |
| 754 | + async queueMessageHandler(data, meta, connectionOptions) { |
623 | 755 | let accountData = await this.accountObject.loadAccountData(); |
624 | 756 |
|
625 | 757 | let gatewayData; |
|
0 commit comments