Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: clear stale queues #1952

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 46 additions & 8 deletions .github/workflows/deploy-beta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ name: Deploy BETA/BugBash Feature

on:
workflow_dispatch:
pull_request:
branches: ['main']
types:
- opened
- synchronize
- reopened

permissions:
id-token: write # allows the JWT to be requested from GitHub's OIDC provider
Expand All @@ -13,37 +19,69 @@ env:
jobs:
get-deploy-inputs:
name: Get Deploy Inputs
if: startsWith(github.ref, 'refs/heads/beta/') || startsWith(github.ref, 'refs/tags/bugbash/')
if: startsWith(github.ref, 'refs/heads/beta/') || startsWith(github.ref, 'refs/tags/bugbash/') || startsWith(github.head_ref, 'beta/')
runs-on: [self-hosted, Linux, X64]
outputs:
release_type: ${{ steps.deploy-inputs.outputs.release_type }}
feature_name: ${{ steps.deploy-inputs.outputs.feature_name }}
latest_feature_name: ${{ steps.deploy-inputs.outputs.latest_feature_name }}
commit_feature_name: ${{ steps.deploy-inputs.outputs.commit_feature_name }}

steps:
- name: Extract deploy inputs
id: deploy-inputs
shell: bash
run: |
source_branch_name=${GITHUB_REF##*/}
source_branch_name=${{ github.head_ref }}

echo "source_branch_name=$source_branch_name"
RELEASE_TYPE=beta
grep -q "bugbash/" <<< "${GITHUB_REF}" && RELEASE_TYPE=bugbash
grep -q "bugbash/" <<< "${${{ github.head_ref }}}" && RELEASE_TYPE=bugbash
FEATURE_NAME=${source_branch_name#bugbash/}
FEATURE_NAME=${FEATURE_NAME#beta/}
FEATURE_NAME=${FEATURE_NAME#refs/heads/}
FEATURE_NAME=${FEATURE_NAME#refs/tags/}

echo "feature_name=$FEATURE_NAME"

LATEST_FEATURE_NAME="${FEATURE_NAME}/latest"
COMMIT_FEATURE_NAME="${FEATURE_NAME}/$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)"

echo "release_type=$RELEASE_TYPE" >> $GITHUB_OUTPUT
echo "feature_name=$FEATURE_NAME" >> $GITHUB_OUTPUT
echo "latest_feature_name=$LATEST_FEATURE_NAME" >> $GITHUB_OUTPUT
echo "commit_feature_name=$COMMIT_FEATURE_NAME" >> $GITHUB_OUTPUT

echo "release_type=$RELEASE_TYPE"
echo "latest_feature_name=$LATEST_FEATURE_NAME"
echo "commit_feature_name=$COMMIT_FEATURE_NAME"

deploy-latest:
name: Deploy BETA/BugBash Feature
uses: ./.github/workflows/deploy.yml
needs: get-deploy-inputs
with:
environment: ${{ needs.get-deploy-inputs.outputs.release_type }}
bugsnag_release_stage: ${{ needs.get-deploy-inputs.outputs.release_type }}
s3_dir_path: ${{ needs.get-deploy-inputs.outputs.release_type }}/${{ needs.get-deploy-inputs.outputs.latest_feature_name }}
s3_dir_path_legacy: ${{ needs.get-deploy-inputs.outputs.release_type }}/${{ needs.get-deploy-inputs.outputs.latest_feature_name }}/v1.1
action_type: ''
secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PROD_ACCOUNT_ID }}
AWS_S3_BUCKET_NAME: ${{ secrets.AWS_PROD_S3_BUCKET_NAME }}
AWS_S3_SYNC_ROLE: ${{ secrets.AWS_PROD_S3_SYNC_ROLE }}
AWS_CF_DISTRIBUTION_ID: ${{ secrets.AWS_PROD_CF_DISTRIBUTION_ID }}
BUGSNAG_API_KEY: ${{ secrets.RS_PROD_BUGSNAG_API_KEY }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_RELEASE_CHANNEL_ID: ${{ secrets.SLACK_RELEASE_CHANNEL_ID_NON_PROD }}

deploy:
deploy-commit:
name: Deploy BETA/BugBash Feature
uses: ./.github/workflows/deploy.yml
needs: get-deploy-inputs
with:
environment: ${{ needs.get-deploy-inputs.outputs.release_type }}
bugsnag_release_stage: ${{ needs.get-deploy-inputs.outputs.release_type }}
s3_dir_path: ${{ needs.get-deploy-inputs.outputs.release_type }}/${{ needs.get-deploy-inputs.outputs.feature_name }}
s3_dir_path_legacy: ${{ needs.get-deploy-inputs.outputs.release_type }}/${{ needs.get-deploy-inputs.outputs.feature_name }}/v1.1
s3_dir_path: ${{ needs.get-deploy-inputs.outputs.release_type }}/${{ needs.get-deploy-inputs.outputs.commit_feature_name }}
s3_dir_path_legacy: ${{ needs.get-deploy-inputs.outputs.release_type }}/${{ needs.get-deploy-inputs.outputs.commit_feature_name }}/v1.1
action_type: ''
secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PROD_ACCOUNT_ID }}
Expand Down
52 changes: 27 additions & 25 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ jobs:
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
BUGSNAG_RELEASE_STAGE: ${{ inputs.bugsnag_release_stage }}
run: |
npm run build:browser
npm run build:browser:modern
# npm run build:browser
npm run build:browser:modern -- --projects=@rudderstack/analytics-js-plugins,@rudderstack/analytics-js

- name: Sync Adobe Analytics assets to S3
if: ${{ inputs.environment == 'production' }}
Expand All @@ -137,40 +137,40 @@ jobs:
integration_sdks_html_file="list.html"
plugins_html_file="list.html"

# Generate a zip file of all the integrations
tmp_file="/tmp/legacy_$integration_sdks_zip_file"
tar -czvf "$tmp_file" -C "$integration_sdks_path_prefix/legacy/js-integrations/" .
mv "$tmp_file" "$integration_sdks_path_prefix/legacy/js-integrations/$integration_sdks_zip_file"
# # Generate a zip file of all the integrations
# tmp_file="/tmp/legacy_$integration_sdks_zip_file"
# tar -czvf "$tmp_file" -C "$integration_sdks_path_prefix/legacy/js-integrations/" .
# mv "$tmp_file" "$integration_sdks_path_prefix/legacy/js-integrations/$integration_sdks_zip_file"

tmp_file="/tmp/modern_$integration_sdks_zip_file"
tar -czvf "$tmp_file" -C "$integration_sdks_path_prefix/modern/js-integrations/" .
mv "$tmp_file" "$integration_sdks_path_prefix/modern/js-integrations/$integration_sdks_zip_file"
# tmp_file="/tmp/modern_$integration_sdks_zip_file"
# tar -czvf "$tmp_file" -C "$integration_sdks_path_prefix/modern/js-integrations/" .
# mv "$tmp_file" "$integration_sdks_path_prefix/modern/js-integrations/$integration_sdks_zip_file"

# Generate a zip file of all the plugins
tmp_file="/tmp/$plugins_zip_file"
tar -czvf "$tmp_file" -C $plugins_path_prefix/modern/plugins/ .
mv "$tmp_file" "$plugins_path_prefix/modern/plugins/$plugins_zip_file"
# # Generate a zip file of all the plugins
# tmp_file="/tmp/$plugins_zip_file"
# tar -czvf "$tmp_file" -C $plugins_path_prefix/modern/plugins/ .
# mv "$tmp_file" "$plugins_path_prefix/modern/plugins/$plugins_zip_file"

# Upload all the files to S3
aws s3 cp $core_sdk_path_prefix/legacy/iife/ $s3_path_prefix/legacy/ $copy_args
aws s3 cp $integration_sdks_path_prefix/legacy/js-integrations/ $s3_path_prefix/legacy/js-integrations/ $copy_args
# aws s3 cp $core_sdk_path_prefix/legacy/iife/ $s3_path_prefix/legacy/ $copy_args
# aws s3 cp $integration_sdks_path_prefix/legacy/js-integrations/ $s3_path_prefix/legacy/js-integrations/ $copy_args

aws s3 cp $core_sdk_path_prefix/modern/iife/ $s3_path_prefix/modern/ $copy_args
aws s3 cp $plugins_path_prefix/modern/plugins/ $s3_path_prefix/modern/plugins/ $copy_args
aws s3 cp $integration_sdks_path_prefix/modern/js-integrations/ $s3_path_prefix/modern/js-integrations/ $copy_args
# aws s3 cp $integration_sdks_path_prefix/modern/js-integrations/ $s3_path_prefix/modern/js-integrations/ $copy_args

# Generate the HTML file to list all the integrations
./scripts/list-sdk-components.sh ${{ secrets.AWS_S3_BUCKET_NAME }} ${{ inputs.s3_dir_path }}/legacy/js-integrations $integration_sdks_html_file $integration_sdks_path_prefix/legacy/js-integrations "Device Mode Integrations" $integration_sdks_zip_file
# # Generate the HTML file to list all the integrations
# ./scripts/list-sdk-components.sh ${{ secrets.AWS_S3_BUCKET_NAME }} ${{ inputs.s3_dir_path }}/legacy/js-integrations $integration_sdks_html_file $integration_sdks_path_prefix/legacy/js-integrations "Device Mode Integrations" $integration_sdks_zip_file

./scripts/list-sdk-components.sh ${{ secrets.AWS_S3_BUCKET_NAME }} ${{ inputs.s3_dir_path }}/modern/js-integrations $integration_sdks_html_file $integration_sdks_path_prefix/modern/js-integrations "Device Mode Integrations" $integration_sdks_zip_file
# ./scripts/list-sdk-components.sh ${{ secrets.AWS_S3_BUCKET_NAME }} ${{ inputs.s3_dir_path }}/modern/js-integrations $integration_sdks_html_file $integration_sdks_path_prefix/modern/js-integrations "Device Mode Integrations" $integration_sdks_zip_file

# Generate the HTML file to list all the plugins
./scripts/list-sdk-components.sh ${{ secrets.AWS_S3_BUCKET_NAME }} ${{ inputs.s3_dir_path }}/modern/plugins $plugins_html_file $plugins_path_prefix/modern/plugins "Plugins" $plugins_zip_file
# # Generate the HTML file to list all the plugins
# ./scripts/list-sdk-components.sh ${{ secrets.AWS_S3_BUCKET_NAME }} ${{ inputs.s3_dir_path }}/modern/plugins $plugins_html_file $plugins_path_prefix/modern/plugins "Plugins" $plugins_zip_file

# Copy the HTML files to S3
aws s3 cp $integration_sdks_path_prefix/legacy/js-integrations/$integration_sdks_html_file $s3_path_prefix/legacy/js-integrations/$integration_sdks_html_file --cache-control ${{ env.CACHE_CONTROL }}
aws s3 cp $integration_sdks_path_prefix/modern/js-integrations/$integration_sdks_html_file $s3_path_prefix/modern/js-integrations/$integration_sdks_html_file --cache-control ${{ env.CACHE_CONTROL }}
aws s3 cp $plugins_path_prefix/modern/plugins/$plugins_html_file $s3_path_prefix/modern/plugins/$plugins_html_file --cache-control ${{ env.CACHE_CONTROL }}
# # Copy the HTML files to S3
# aws s3 cp $integration_sdks_path_prefix/legacy/js-integrations/$integration_sdks_html_file $s3_path_prefix/legacy/js-integrations/$integration_sdks_html_file --cache-control ${{ env.CACHE_CONTROL }}
# aws s3 cp $integration_sdks_path_prefix/modern/js-integrations/$integration_sdks_html_file $s3_path_prefix/modern/js-integrations/$integration_sdks_html_file --cache-control ${{ env.CACHE_CONTROL }}
# aws s3 cp $plugins_path_prefix/modern/plugins/$plugins_html_file $s3_path_prefix/modern/plugins/$plugins_html_file --cache-control ${{ env.CACHE_CONTROL }}

- name: Create Cloudfront invalidation
run: |
Expand Down Expand Up @@ -264,6 +264,7 @@ jobs:
# Below steps are for v1.1 SDK (legacy)

- name: Sync files to S3 v1.1 directory
if: ${{ inputs.environment == 'production' }}
run: |
core_sdk_path_prefix="packages/analytics-v1.1/dist/cdn"
integration_sdks_path_prefix="packages/analytics-js-integrations/dist/cdn"
Expand All @@ -277,6 +278,7 @@ jobs:
aws s3 cp $integration_sdks_path_prefix/modern/js-integrations/ $s3_path_prefix/modern/js-integrations/ $copy_args

- name: Create Cloudfront invalidation
if: ${{ inputs.environment == 'production' }}
run: |
AWS_MAX_ATTEMPTS=10 aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_CF_DISTRIBUTION_ID }} --paths "/${{ inputs.s3_dir_path_legacy }}*"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Security, Code Quality and Bundle Size Checks

on:
pull_request:
branches: ['develop', 'main', 'hotfix/*']
branches: ['develop', 'hotfix/*']
types: ['opened', 'reopened', 'synchronize']

env:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
],
"scripts": {
"setup": "npm i --include=optional && npm run build:package:modern",
"setup:ci": "npm ci && npm i @nx/nx-linux-x64-gnu && npm run build:package:modern",
"setup:ci": "npm ci && npm i @nx/nx-linux-x64-gnu",
"clean": "nx run-many -t clean && nx reset && git clean -xdf node_modules",
"clean:cache": "rimraf -rf ./node_modules/.cache && rimraf -rf ./.nx/cache",
"start": "nx run-many --targets=start --parallel=3 --projects=@rudderstack/analytics-js-integrations,@rudderstack/analytics-js-plugins,@rudderstack/analytics-js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const NativeDestinationQueue = (): ExtensionPlugin => ({
},
storeManager,
MEMORY_STORAGE,
logger
);

// TODO: This seems to not work as expected. Need to investigate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,12 @@ class RetryQueue implements IQueue<QueueItemData> {
let queue =
(this.getStorageEntry(QueueStatuses.QUEUE) as Nullable<QueueItem<QueueItemData>[]>) ?? [];

queue = queue.slice(-(this.maxItems - 1));
if (this.maxItems > 1) {
queue = queue.slice(-(this.maxItems - 1));
} else {
queue = [];
}

Comment on lines +374 to +379
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix potential queue size management issue.

The current implementation has a logic flaw in queue size management. When maxItems is 1, it clears the queue before adding the new item, which could lead to data loss if the new item fails to be added.

-    if (this.maxItems > 1) {
-      queue = queue.slice(-(this.maxItems - 1));
-    } else {
-      queue = [];
-    }
+    // Ensure we maintain the most recent items up to maxItems
+    queue = queue.slice(-this.maxItems);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (this.maxItems > 1) {
queue = queue.slice(-(this.maxItems - 1));
} else {
queue = [];
}
// Ensure we maintain the most recent items up to maxItems
queue = queue.slice(-this.maxItems);

queue.push(curEntry);
queue = queue.sort(sortByTime);

Expand Down Expand Up @@ -541,7 +546,16 @@ class RetryQueue implements IQueue<QueueItemData> {
const willBeRetried = this.shouldRetry(el.item, el.attemptNumber + 1);
this.processQueueCb(el.item, el.done, el.attemptNumber, this.maxAttempts, willBeRetried);
} catch (err) {
// drop the event from in progress queue as we're unable to process it
el.done();
this.logger?.error(RETRY_QUEUE_PROCESS_ERROR(RETRY_QUEUE), err);
this.logger?.error('Debugging data dump starts');
this.logger?.error('Queue item', el);
this.logger?.error('RetryQueue Instance', this);
this.logger?.error('Primary Queue', this.getStorageEntry(QueueStatuses.QUEUE));
this.logger?.error('In-Progress Queue', this.getStorageEntry(QueueStatuses.IN_PROGRESS));
this.logger?.error('RudderStack Globals', (globalThis as typeof window).RudderStackGlobals);
this.logger?.error('Debugging data dump ends');
}
});

Expand Down Expand Up @@ -584,9 +598,9 @@ class RetryQueue implements IQueue<QueueItemData> {
validKeys: QueueStatuses,
type: LOCAL_STORAGE,
});
const our = {
queue: (this.getStorageEntry(QueueStatuses.QUEUE) ?? []) as QueueItem[],
};

const reclaimedQueueItems: QueueItem[] = [];

const their = {
inProgress: other.get(QueueStatuses.IN_PROGRESS) ?? {},
batchQueue: other.get(QueueStatuses.BATCH_QUEUE) ?? [],
Expand All @@ -609,7 +623,7 @@ class RetryQueue implements IQueue<QueueItemData> {
// and the new entries will have the type field set
const type = Array.isArray(el.item) ? BATCH_QUEUE_ITEM_TYPE : SINGLE_QUEUE_ITEM_TYPE;

our.queue.push({
reclaimedQueueItems.push({
item: el.item,
attemptNumber: el.attemptNumber + incrementAttemptNumberBy,
time: this.schedule.now(),
Expand Down Expand Up @@ -652,9 +666,15 @@ class RetryQueue implements IQueue<QueueItemData> {
// if the queue is abandoned, all the in-progress are failed. retry them immediately and increment the attempt#
addConcatQueue(their.inProgress, 1);

our.queue = our.queue.sort(sortByTime);
let ourQueue = (this.getStorageEntry(QueueStatuses.QUEUE) as QueueItem[]) ?? [];
const roomInQueue = Math.max(0, this.maxItems - ourQueue.length);
if (roomInQueue > 0) {
ourQueue.push(...reclaimedQueueItems.slice(0, roomInQueue));
}

ourQueue = ourQueue.sort(sortByTime);

this.setStorageEntry(QueueStatuses.QUEUE, our.queue);
this.setStorageEntry(QueueStatuses.QUEUE, ourQueue);

// remove all keys one by on next tick to avoid NS_ERROR_STORAGE_BUSY error
this.clearQueueEntries(other, 1);
Expand Down Expand Up @@ -704,40 +724,6 @@ class RetryQueue implements IQueue<QueueItemData> {
}

checkReclaim() {
const createReclaimStartTask = (store: IStore) => () => {
if (store.get(QueueStatuses.RECLAIM_END) !== this.id) {
return;
}

if (store.get(QueueStatuses.RECLAIM_START) !== this.id) {
return;
}

this.reclaim(store.id);
};
const createReclaimEndTask = (store: IStore) => () => {
if (store.get(QueueStatuses.RECLAIM_START) !== this.id) {
return;
}

store.set(QueueStatuses.RECLAIM_END, this.id);

this.schedule.run(
createReclaimStartTask(store),
this.timeouts.reclaimWait,
ScheduleModes.ABANDON,
);
};
const tryReclaim = (store: IStore) => {
store.set(QueueStatuses.RECLAIM_START, this.id);
store.set(QueueStatuses.ACK, this.schedule.now());

this.schedule.run(
createReclaimEndTask(store),
this.timeouts.reclaimWait,
ScheduleModes.ABANDON,
);
};
const findOtherQueues = (name: string): IStore[] => {
const res: IStore[] = [];
const storageEngine = this.store.getOriginalEngine();
Expand Down Expand Up @@ -778,15 +764,8 @@ class RetryQueue implements IQueue<QueueItemData> {
return res;
};

findOtherQueues(this.name).forEach(store => {
if (this.schedule.now() - store.get(QueueStatuses.ACK) < this.timeouts.reclaimTimeout) {
return;
}

tryReclaim(store);
});

this.schedule.run(this.checkReclaim, this.timeouts.reclaimTimer, ScheduleModes.RESCHEDULE);
// Instead of reclaiming stale queues, clear them
findOtherQueues(this.name).forEach(store => this.clearQueueEntries(store, 0));
Comment on lines +830 to +831
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential data loss by clearing stale queues instead of reclaiming

Changing the checkReclaim method to clear stale queues (clearQueueEntries) rather than reclaiming them could lead to loss of unprocessed queue items, resulting in possible data loss.

Consider evaluating whether it's acceptable to discard these items. If not, you might need to implement a mechanism to reclaim unprocessed items from stale queues to ensure data integrity.

}

clear() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { LOG_CONTEXT_SEPARATOR } from '../../shared-chunks/common';

const RETRY_QUEUE_PROCESS_ERROR = (context: string): string =>
`${context}${LOG_CONTEXT_SEPARATOR}Process function threw an error.`;
`${context}${LOG_CONTEXT_SEPARATOR}Process function threw an error while processing the queue item. The item is be dropped.`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix grammatical error in error message

The error message contains a grammatical error: "The item is be dropped". This should be either "The item is dropped" or "The item will be dropped".

Apply this fix:

-  `${context}${LOG_CONTEXT_SEPARATOR}Process function threw an error while processing the queue item. The item is be dropped.`;
+  `${context}${LOG_CONTEXT_SEPARATOR}Process function threw an error while processing the queue item. The item will be dropped.`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`${context}${LOG_CONTEXT_SEPARATOR}Process function threw an error while processing the queue item. The item is be dropped.`;
`${context}${LOG_CONTEXT_SEPARATOR}Process function threw an error while processing the queue item. The item will be dropped.`;


const RETRY_QUEUE_ENTRY_REMOVE_ERROR = (context: string, entry: string, attempt: number): string =>
`${context}${LOG_CONTEXT_SEPARATOR}Failed to remove local storage entry "${entry}" (attempt: ${attempt}.`;
Expand Down
Loading