mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
improvement: limit app connection concurrent syncs
This commit is contained in:
@@ -38,6 +38,7 @@ export const KeyStorePrefixes = {
|
|||||||
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
|
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
|
||||||
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const,
|
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const,
|
||||||
SecretSyncLock: (syncId: string) => `secret-sync-mutex-${syncId}` as const,
|
SecretSyncLock: (syncId: string) => `secret-sync-mutex-${syncId}` as const,
|
||||||
|
AppConnectionConcurrentJobs: (connectionId: string) => `app-connection-concurrency-${connectionId}` as const,
|
||||||
SecretRotationLock: (rotationId: string) => `secret-rotation-v2-mutex-${rotationId}` as const,
|
SecretRotationLock: (rotationId: string) => `secret-rotation-v2-mutex-${rotationId}` as const,
|
||||||
SecretScanningLock: (dataSourceId: string, resourceExternalId: string) =>
|
SecretScanningLock: (dataSourceId: string, resourceExternalId: string) =>
|
||||||
`secret-scanning-v2-mutex-${dataSourceId}-${resourceExternalId}` as const,
|
`secret-scanning-v2-mutex-${dataSourceId}-${resourceExternalId}` as const,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ type TAWSParameterStoreRecord = Record<string, AWS.SSM.Parameter>;
|
|||||||
type TAWSParameterStoreMetadataRecord = Record<string, AWS.SSM.ParameterMetadata>;
|
type TAWSParameterStoreMetadataRecord = Record<string, AWS.SSM.ParameterMetadata>;
|
||||||
type TAWSParameterStoreTagsRecord = Record<string, Record<string, string>>;
|
type TAWSParameterStoreTagsRecord = Record<string, Record<string, string>>;
|
||||||
|
|
||||||
const MAX_RETRIES = 5;
|
const MAX_RETRIES = 10;
|
||||||
const BATCH_SIZE = 10;
|
const BATCH_SIZE = 10;
|
||||||
|
|
||||||
const getSSM = async (secretSync: TAwsParameterStoreSyncWithCredentials) => {
|
const getSSM = async (secretSync: TAwsParameterStoreSyncWithCredentials) => {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ type TAwsSecretsRecord = Record<string, SecretListEntry>;
|
|||||||
type TAwsSecretValuesRecord = Record<string, SecretValueEntry>;
|
type TAwsSecretValuesRecord = Record<string, SecretValueEntry>;
|
||||||
type TAwsSecretDescriptionsRecord = Record<string, DescribeSecretResponse>;
|
type TAwsSecretDescriptionsRecord = Record<string, DescribeSecretResponse>;
|
||||||
|
|
||||||
const MAX_RETRIES = 5;
|
const MAX_RETRIES = 10;
|
||||||
const BATCH_SIZE = 20;
|
const BATCH_SIZE = 20;
|
||||||
|
|
||||||
const getSecretsManagerClient = async (secretSync: TAwsSecretsManagerSyncWithCredentials) => {
|
const getSecretsManagerClient = async (secretSync: TAwsSecretsManagerSyncWithCredentials) => {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ type TSecretSyncQueueFactoryDep = {
|
|||||||
| "invalidateSecretCacheByProjectId"
|
| "invalidateSecretCacheByProjectId"
|
||||||
>;
|
>;
|
||||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||||
secretSyncDAL: Pick<TSecretSyncDALFactory, "findById" | "find" | "updateById" | "deleteById">;
|
secretSyncDAL: Pick<TSecretSyncDALFactory, "findById" | "find" | "updateById" | "deleteById" | "update">;
|
||||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
@@ -104,17 +104,15 @@ type SecretSyncActionJob = Job<
|
|||||||
TQueueSecretSyncSyncSecretsByIdDTO | TQueueSecretSyncImportSecretsByIdDTO | TQueueSecretSyncRemoveSecretsByIdDTO
|
TQueueSecretSyncSyncSecretsByIdDTO | TQueueSecretSyncImportSecretsByIdDTO | TQueueSecretSyncRemoveSecretsByIdDTO
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
const JITTER_MS = 10 * 1000;
|
||||||
|
const REQUEUE_MS = 30 * 1000;
|
||||||
|
const REQUEUE_LIMIT = 30;
|
||||||
|
const CONNECTION_CONCURRENCY_LIMIT = 3;
|
||||||
|
|
||||||
const getRequeueDelay = (failureCount?: number) => {
|
const getRequeueDelay = (failureCount?: number) => {
|
||||||
if (!failureCount) return 0;
|
const jitter = Math.random() * JITTER_MS;
|
||||||
|
if (!failureCount) return jitter;
|
||||||
const baseDelay = 1000;
|
return REQUEUE_MS + jitter;
|
||||||
const maxDelay = 30000;
|
|
||||||
|
|
||||||
const delay = Math.min(baseDelay * 2 ** failureCount, maxDelay);
|
|
||||||
|
|
||||||
const jitter = delay * (0.5 + Math.random() * 0.5);
|
|
||||||
|
|
||||||
return jitter;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const secretSyncQueueFactory = ({
|
export const secretSyncQueueFactory = ({
|
||||||
@@ -193,6 +191,46 @@ export const secretSyncQueueFactory = ({
|
|||||||
folderCommitService
|
folderCommitService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const $isConnectionConcurrencyLimitReached = async (connectionId: string) => {
|
||||||
|
const concurrencyCount = await keyStore.getItem(KeyStorePrefixes.AppConnectionConcurrentJobs(connectionId));
|
||||||
|
|
||||||
|
if (!concurrencyCount) return false;
|
||||||
|
|
||||||
|
const count = Number.parseInt(concurrencyCount, 10);
|
||||||
|
|
||||||
|
if (Number.isNaN(count)) return false;
|
||||||
|
|
||||||
|
return count >= CONNECTION_CONCURRENCY_LIMIT;
|
||||||
|
};
|
||||||
|
|
||||||
|
const $incrementConnectionConcurrencyCount = async (connectionId: string) => {
|
||||||
|
const concurrencyCount = await keyStore.getItem(KeyStorePrefixes.AppConnectionConcurrentJobs(connectionId));
|
||||||
|
|
||||||
|
const currentCount = Number.parseInt(concurrencyCount || "0", 10);
|
||||||
|
|
||||||
|
const incrementedCount = Number.isNaN(currentCount) ? 1 : currentCount + 1;
|
||||||
|
|
||||||
|
await keyStore.setItemWithExpiry(
|
||||||
|
KeyStorePrefixes.AppConnectionConcurrentJobs(connectionId),
|
||||||
|
(REQUEUE_MS * REQUEUE_LIMIT) / 1000, // in seconds
|
||||||
|
incrementedCount
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const $decrementConnectionConcurrencyCount = async (connectionId: string) => {
|
||||||
|
const concurrencyCount = await keyStore.getItem(KeyStorePrefixes.AppConnectionConcurrentJobs(connectionId));
|
||||||
|
|
||||||
|
const currentCount = Number.parseInt(concurrencyCount || "0", 10);
|
||||||
|
|
||||||
|
const decrementedCount = Math.max(0, Number.isNaN(currentCount) ? 0 : currentCount - 1);
|
||||||
|
|
||||||
|
await keyStore.setItemWithExpiry(
|
||||||
|
KeyStorePrefixes.AppConnectionConcurrentJobs(connectionId),
|
||||||
|
(REQUEUE_MS * REQUEUE_LIMIT) / 1000, // in seconds
|
||||||
|
decrementedCount
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const $getInfisicalSecrets = async (
|
const $getInfisicalSecrets = async (
|
||||||
secretSync: TSecretSyncRaw | TSecretSyncWithCredentials,
|
secretSync: TSecretSyncRaw | TSecretSyncWithCredentials,
|
||||||
includeImports = true
|
includeImports = true
|
||||||
@@ -416,15 +454,11 @@ export const secretSyncQueueFactory = ({
|
|||||||
return importedSecretMap;
|
return importedSecretMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
const $handleSyncSecretsJob = async (job: TSecretSyncSyncSecretsDTO) => {
|
const $handleSyncSecretsJob = async (job: TSecretSyncSyncSecretsDTO, secretSync: TSecretSyncRaw) => {
|
||||||
const {
|
const {
|
||||||
data: { syncId, auditLogInfo }
|
data: { syncId, auditLogInfo }
|
||||||
} = job;
|
} = job;
|
||||||
|
|
||||||
const secretSync = await secretSyncDAL.findById(syncId);
|
|
||||||
|
|
||||||
if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
|
|
||||||
|
|
||||||
await enterpriseSyncCheck(
|
await enterpriseSyncCheck(
|
||||||
licenseService,
|
licenseService,
|
||||||
secretSync.destination as SecretSync,
|
secretSync.destination as SecretSync,
|
||||||
@@ -566,15 +600,11 @@ export const secretSyncQueueFactory = ({
|
|||||||
logger.info("SecretSync Sync Job with ID %s Completed", job.id);
|
logger.info("SecretSync Sync Job with ID %s Completed", job.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const $handleImportSecretsJob = async (job: TSecretSyncImportSecretsDTO) => {
|
const $handleImportSecretsJob = async (job: TSecretSyncImportSecretsDTO, secretSync: TSecretSyncRaw) => {
|
||||||
const {
|
const {
|
||||||
data: { syncId, auditLogInfo, importBehavior }
|
data: { syncId, auditLogInfo, importBehavior }
|
||||||
} = job;
|
} = job;
|
||||||
|
|
||||||
const secretSync = await secretSyncDAL.findById(syncId);
|
|
||||||
|
|
||||||
if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
|
|
||||||
|
|
||||||
await secretSyncDAL.updateById(syncId, {
|
await secretSyncDAL.updateById(syncId, {
|
||||||
importStatus: SecretSyncStatus.Running
|
importStatus: SecretSyncStatus.Running
|
||||||
});
|
});
|
||||||
@@ -683,15 +713,11 @@ export const secretSyncQueueFactory = ({
|
|||||||
logger.info("SecretSync Import Job with ID %s Completed", job.id);
|
logger.info("SecretSync Import Job with ID %s Completed", job.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const $handleRemoveSecretsJob = async (job: TSecretSyncRemoveSecretsDTO) => {
|
const $handleRemoveSecretsJob = async (job: TSecretSyncRemoveSecretsDTO, secretSync: TSecretSyncRaw) => {
|
||||||
const {
|
const {
|
||||||
data: { syncId, auditLogInfo, deleteSyncOnComplete }
|
data: { syncId, auditLogInfo, deleteSyncOnComplete }
|
||||||
} = job;
|
} = job;
|
||||||
|
|
||||||
const secretSync = await secretSyncDAL.findById(syncId);
|
|
||||||
|
|
||||||
if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
|
|
||||||
|
|
||||||
await enterpriseSyncCheck(
|
await enterpriseSyncCheck(
|
||||||
licenseService,
|
licenseService,
|
||||||
secretSync.destination as SecretSync,
|
secretSync.destination as SecretSync,
|
||||||
@@ -894,6 +920,17 @@ export const secretSyncQueueFactory = ({
|
|||||||
|
|
||||||
const secretSyncs = await secretSyncDAL.find({ folderId: folder.id, isAutoSyncEnabled: true });
|
const secretSyncs = await secretSyncDAL.find({ folderId: folder.id, isAutoSyncEnabled: true });
|
||||||
|
|
||||||
|
await secretSyncDAL.update(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
id: secretSyncs.map((sync) => sync.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
syncStatus: SecretSyncStatus.Pending
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
await Promise.all(secretSyncs.map((secretSync) => queueSecretSyncSyncSecretsById({ syncId: secretSync.id })));
|
await Promise.all(secretSyncs.map((secretSync) => queueSecretSyncSyncSecretsById({ syncId: secretSync.id })));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -904,7 +941,7 @@ export const secretSyncQueueFactory = ({
|
|||||||
case QueueJobs.SecretSyncSyncSecrets: {
|
case QueueJobs.SecretSyncSyncSecrets: {
|
||||||
const { failedToAcquireLockCount = 0, ...rest } = job.data as TQueueSecretSyncSyncSecretsByIdDTO;
|
const { failedToAcquireLockCount = 0, ...rest } = job.data as TQueueSecretSyncSyncSecretsByIdDTO;
|
||||||
|
|
||||||
if (failedToAcquireLockCount < 10) {
|
if (failedToAcquireLockCount < REQUEUE_LIMIT) {
|
||||||
await queueSecretSyncSyncSecretsById({ ...rest, failedToAcquireLockCount: failedToAcquireLockCount + 1 });
|
await queueSecretSyncSyncSecretsById({ ...rest, failedToAcquireLockCount: failedToAcquireLockCount + 1 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -974,6 +1011,26 @@ export const secretSyncQueueFactory = ({
|
|||||||
| TQueueSecretSyncImportSecretsByIdDTO
|
| TQueueSecretSyncImportSecretsByIdDTO
|
||||||
| TQueueSecretSyncRemoveSecretsByIdDTO;
|
| TQueueSecretSyncRemoveSecretsByIdDTO;
|
||||||
|
|
||||||
|
const secretSync = await secretSyncDAL.findById(syncId);
|
||||||
|
|
||||||
|
if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
|
||||||
|
|
||||||
|
const { connectionId } = secretSync;
|
||||||
|
|
||||||
|
if (job.name === QueueJobs.SecretSyncSyncSecrets) {
|
||||||
|
const isConcurrentLimitReached = await $isConnectionConcurrencyLimitReached(connectionId);
|
||||||
|
|
||||||
|
if (isConcurrentLimitReached) {
|
||||||
|
logger.info(
|
||||||
|
`SecretSync Concurrency limit reached [syncId=${syncId}] [job=${job.name}] [connectionId=${connectionId}]`
|
||||||
|
);
|
||||||
|
|
||||||
|
await $handleAcquireLockFailure(job as SecretSyncActionJob);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>>;
|
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>>;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -993,20 +1050,26 @@ export const secretSyncQueueFactory = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
switch (job.name) {
|
switch (job.name) {
|
||||||
case QueueJobs.SecretSyncSyncSecrets:
|
case QueueJobs.SecretSyncSyncSecrets: {
|
||||||
await $handleSyncSecretsJob(job as TSecretSyncSyncSecretsDTO);
|
await $incrementConnectionConcurrencyCount(connectionId);
|
||||||
|
await $handleSyncSecretsJob(job as TSecretSyncSyncSecretsDTO, secretSync);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case QueueJobs.SecretSyncImportSecrets:
|
case QueueJobs.SecretSyncImportSecrets:
|
||||||
await $handleImportSecretsJob(job as TSecretSyncImportSecretsDTO);
|
await $handleImportSecretsJob(job as TSecretSyncImportSecretsDTO, secretSync);
|
||||||
break;
|
break;
|
||||||
case QueueJobs.SecretSyncRemoveSecrets:
|
case QueueJobs.SecretSyncRemoveSecrets:
|
||||||
await $handleRemoveSecretsJob(job as TSecretSyncRemoveSecretsDTO);
|
await $handleRemoveSecretsJob(job as TSecretSyncRemoveSecretsDTO, secretSync);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
throw new Error(`Unhandled Secret Sync Job ${job.name}`);
|
throw new Error(`Unhandled Secret Sync Job ${job.name}`);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if (job.name === QueueJobs.SecretSyncSyncSecrets) {
|
||||||
|
await $decrementConnectionConcurrencyCount(connectionId);
|
||||||
|
}
|
||||||
|
|
||||||
await lock.release();
|
await lock.release();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
faCheck,
|
faCheck,
|
||||||
faExclamationTriangle,
|
faExclamationTriangle,
|
||||||
|
faHourglass,
|
||||||
faRotate,
|
faRotate,
|
||||||
IconDefinition
|
IconDefinition
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -29,7 +30,11 @@ export const SecretSyncStatusBadge = ({ status }: Props) => {
|
|||||||
text = "Synced";
|
text = "Synced";
|
||||||
icon = faCheck;
|
icon = faCheck;
|
||||||
break;
|
break;
|
||||||
case SecretSyncStatus.Pending: // no need to differentiate from user perspective
|
case SecretSyncStatus.Pending:
|
||||||
|
variant = "primary";
|
||||||
|
text = "Queued";
|
||||||
|
icon = faHourglass;
|
||||||
|
break;
|
||||||
case SecretSyncStatus.Running:
|
case SecretSyncStatus.Running:
|
||||||
default:
|
default:
|
||||||
variant = "primary";
|
variant = "primary";
|
||||||
@@ -42,11 +47,7 @@ export const SecretSyncStatusBadge = ({ status }: Props) => {
|
|||||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap" variant={variant}>
|
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap" variant={variant}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={icon}
|
icon={icon}
|
||||||
className={
|
className={[SecretSyncStatus.Running].includes(status) ? "animate-spin" : ""}
|
||||||
[SecretSyncStatus.Pending, SecretSyncStatus.Running].includes(status)
|
|
||||||
? "animate-spin"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<span>{text}</span>
|
<span>{text}</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
Reference in New Issue
Block a user