feat: added queue level debounce for secret sync and removed stale check in sync

This commit is contained in:
=
2024-11-06 16:29:03 +05:30
parent ea426e8b2d
commit 3983c2bc4a
2 changed files with 82 additions and 76 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ export const KeyStorePrefixes = {
}; };
export const KeyStoreTtls = { export const KeyStoreTtls = {
SetSyncSecretIntegrationLastRunTimestampInSeconds: 10, SetSyncSecretIntegrationLastRunTimestampInSeconds: 60,
AccessTokenStatusUpdateInSeconds: 120 AccessTokenStatusUpdateInSeconds: 120
}; };
+81 -75
View File
@@ -112,6 +112,8 @@ export type TGetSecrets = {
}; };
const MAX_SYNC_SECRET_DEPTH = 5; const MAX_SYNC_SECRET_DEPTH = 5;
const SYNC_SECRET_DEBOUNCE_INTERVAL_MS = 3000;
export const uniqueSecretQueueKey = (environment: string, secretPath: string) => export const uniqueSecretQueueKey = (environment: string, secretPath: string) =>
`secret-queue-dedupe-${environment}-${secretPath}`; `secret-queue-dedupe-${environment}-${secretPath}`;
@@ -169,6 +171,39 @@ export const secretQueueFactory = ({
); );
}; };
const $generateActor = async (actorId?: string, isManual?: boolean): Promise<Actor> => {
if (isManual && actorId) {
const user = await userDAL.findById(actorId);
if (!user) {
throw new Error("User not found");
}
return {
type: ActorType.USER,
metadata: {
email: user.email,
username: user.username,
userId: user.id
}
};
}
return {
type: ActorType.PLATFORM,
metadata: {}
};
};
const $getJobKey = (projectId: string, environmentSlug: string, secretPath: string) => {
// the idea here is a timestamp based id which will be constant in a 3s interval
const timestampId = Math.floor(Date.now() / SYNC_SECRET_DEBOUNCE_INTERVAL_MS);
return `secret-queue-sync_${projectId}_${environmentSlug}_${secretPath}_${timestampId}`
.replace("/", "-")
.replace(":", "-");
};
const addSecretReminder = async ({ oldSecret, newSecret, projectId }: TCreateSecretReminderDTO) => { const addSecretReminder = async ({ oldSecret, newSecret, projectId }: TCreateSecretReminderDTO) => {
try { try {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -466,7 +501,7 @@ export const secretQueueFactory = ({
dto: TGetSecrets & { isManual?: boolean; actorId?: string; deDupeQueue?: Record<string, boolean> } dto: TGetSecrets & { isManual?: boolean; actorId?: string; deDupeQueue?: Record<string, boolean> }
) => { ) => {
await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, { await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, {
attempts: 3, attempts: 5,
delay: 1000, delay: 1000,
backoff: { backoff: {
type: "exponential", type: "exponential",
@@ -479,10 +514,10 @@ export const secretQueueFactory = ({
const replicateSecrets = async (dto: Omit<TSyncSecretsDTO, "deDupeQueue">) => { const replicateSecrets = async (dto: Omit<TSyncSecretsDTO, "deDupeQueue">) => {
await queueService.queue(QueueName.SecretReplication, QueueJobs.SecretReplication, dto, { await queueService.queue(QueueName.SecretReplication, QueueJobs.SecretReplication, dto, {
attempts: 3, attempts: 5,
backoff: { backoff: {
type: "exponential", type: "exponential",
delay: 2000 delay: 3000
}, },
removeOnComplete: true, removeOnComplete: true,
removeOnFail: true removeOnFail: true
@@ -499,6 +534,7 @@ export const secretQueueFactory = ({
logger.info( logger.info(
`syncSecrets: syncing project secrets where [projectId=${dto.projectId}] [environment=${dto.environmentSlug}] [path=${dto.secretPath}]` `syncSecrets: syncing project secrets where [projectId=${dto.projectId}] [environment=${dto.environmentSlug}] [path=${dto.secretPath}]`
); );
const deDuplicationKey = uniqueSecretQueueKey(dto.environmentSlug, dto.secretPath); const deDuplicationKey = uniqueSecretQueueKey(dto.environmentSlug, dto.secretPath);
if ( if (
!dto.excludeReplication !dto.excludeReplication
@@ -523,7 +559,8 @@ export const secretQueueFactory = ({
{ {
removeOnFail: true, removeOnFail: true,
removeOnComplete: true, removeOnComplete: true,
delay: 1000, jobId: $getJobKey(dto.projectId, dto.environmentSlug, dto.secretPath),
delay: SYNC_SECRET_DEBOUNCE_INTERVAL_MS,
attempts: 5, attempts: 5,
backoff: { backoff: {
type: "exponential", type: "exponential",
@@ -532,7 +569,6 @@ export const secretQueueFactory = ({
} }
); );
}; };
const sendFailedIntegrationSyncEmails = async (payload: TFailedIntegrationSyncEmailsPayload) => { const sendFailedIntegrationSyncEmails = async (payload: TFailedIntegrationSyncEmailsPayload) => {
const appCfg = getConfig(); const appCfg = getConfig();
if (!appCfg.isSmtpConfigured) return; if (!appCfg.isSmtpConfigured) return;
@@ -540,7 +576,6 @@ export const secretQueueFactory = ({
await queueService.queue(QueueName.IntegrationSync, QueueJobs.SendFailedIntegrationSyncEmails, payload, { await queueService.queue(QueueName.IntegrationSync, QueueJobs.SendFailedIntegrationSyncEmails, payload, {
jobId: `send-failed-integration-sync-emails-${payload.projectId}-${payload.secretPath}-${payload.environmentSlug}`, jobId: `send-failed-integration-sync-emails-${payload.projectId}-${payload.secretPath}-${payload.environmentSlug}`,
delay: 1_000 * 60, // 1 minute delay: 1_000 * 60, // 1 minute
removeOnFail: true, removeOnFail: true,
removeOnComplete: true removeOnComplete: true
}); });
@@ -733,80 +768,51 @@ export const secretQueueFactory = ({
); );
} }
const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment const lock = await keyStore.acquireLock(
const toBeSyncedIntegrations = integrations.filter( [KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)],
// note: sync only the integrations sourced from secretPath 60000,
({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath) {
retryCount: 10,
retryDelay: 3000,
retryJitter: 500
}
); );
const integrationsFailedToSync: { integrationId: string; syncMessage?: string }[] = []; const integrationsFailedToSync: { integrationId: string; syncMessage?: string }[] = [];
if (!integrations.length) return;
logger.info(
`getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
);
const lock = await keyStore.acquireLock(
[KeyStorePrefixes.SyncSecretIntegrationLock(projectId, environment, secretPath)],
10000,
{
retryCount: 3,
retryDelay: 2000
}
);
const lockAcquiredTime = new Date(); const lockAcquiredTime = new Date();
const lastRunSyncIntegrationTimestamp = await keyStore.getItem(
KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath)
);
// check whether the integration should wait or not
if (lastRunSyncIntegrationTimestamp) {
const INTEGRATION_INTERVAL = 2000;
const isStaleSyncIntegration = new Date(job.timestamp) < new Date(lastRunSyncIntegrationTimestamp);
if (isStaleSyncIntegration) {
logger.info(
`getIntegrationSecrets: secret integration sync stale [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
);
return;
}
const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds(
lockAcquiredTime.toISOString(),
lastRunSyncIntegrationTimestamp
);
if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL && timeDifferenceWithLastIntegration > 0)
await new Promise((resolve) => {
setTimeout(resolve, 2000 - timeDifferenceWithLastIntegration * 1000);
});
}
const generateActor = async (): Promise<Actor> => {
if (isManual && actorId) {
const user = await userDAL.findById(actorId);
if (!user) {
throw new Error("User not found");
}
return {
type: ActorType.USER,
metadata: {
email: user.email,
username: user.username,
userId: user.id
}
};
}
return {
type: ActorType.PLATFORM,
metadata: {}
};
};
// akhilmhdh: this try catch is for lock release // akhilmhdh: this try catch is for lock release
try { try {
const lastRunSyncIntegrationTimestamp = await keyStore.getItem(
KeyStorePrefixes.SyncSecretIntegrationLastRunTimestamp(projectId, environment, secretPath)
);
// check whether the integration should wait or not
if (lastRunSyncIntegrationTimestamp) {
const INTEGRATION_INTERVAL = 2000;
const timeDifferenceWithLastIntegration = getTimeDifferenceInSeconds(
lockAcquiredTime.toISOString(),
lastRunSyncIntegrationTimestamp
);
// give some time for integration to breath
if (timeDifferenceWithLastIntegration < INTEGRATION_INTERVAL)
await new Promise((resolve) => {
setTimeout(resolve, INTEGRATION_INTERVAL);
});
}
const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment
const toBeSyncedIntegrations = integrations.filter(
// note: sync only the integrations sourced from secretPath
({ secretPath: integrationSecPath, isActive }) => isActive && isSamePath(secretPath, integrationSecPath)
);
if (!integrations.length) return;
logger.info(
`getIntegrationSecrets: secret integration sync started [jobId=${job.id}] [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
);
const secrets = shouldUseSecretV2Bridge const secrets = shouldUseSecretV2Bridge
? await getIntegrationSecretsV2({ ? await getIntegrationSecretsV2({
environment, environment,
@@ -892,7 +898,7 @@ export const secretQueueFactory = ({
await auditLogService.createAuditLog({ await auditLogService.createAuditLog({
projectId, projectId,
actor: await generateActor(), actor: await $generateActor(actorId, isManual),
event: { event: {
type: EventType.INTEGRATION_SYNCED, type: EventType.INTEGRATION_SYNCED,
metadata: { metadata: {
@@ -931,7 +937,7 @@ export const secretQueueFactory = ({
await auditLogService.createAuditLog({ await auditLogService.createAuditLog({
projectId, projectId,
actor: await generateActor(), actor: await $generateActor(actorId, isManual),
event: { event: {
type: EventType.INTEGRATION_SYNCED, type: EventType.INTEGRATION_SYNCED,
metadata: { metadata: {