Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "hawk.workers",
"private": true,
"version": "0.1.4",
"version": "0.1.5",
"description": "Hawk workers",
"repository": "git@github.com:codex-team/hawk.workers.git",
"license": "BUSL-1.1",
Expand Down Expand Up @@ -57,7 +57,7 @@
"@babel/parser": "^7.26.9",
"@babel/traverse": "7.26.9",
"@hawk.so/nodejs": "^3.1.1",
"@hawk.so/types": "^0.5.9",
"@hawk.so/types": "^0.7.1",
"@types/amqplib": "^0.8.2",
"@types/jest": "^29.5.14",
"@types/mongodb": "^3.5.15",
Expand Down
89 changes: 61 additions & 28 deletions workers/release/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,21 @@ export default class ReleaseWorker extends Worker {
this.logger.info(`saveRelease: save release for project: ${projectId}, release: ${payload.release}`);
try {
const commits = payload.commits;
const validCommits = !!commits && this.areCommitsValid(commits);
const hasFiles = Array.isArray(payload.files) && payload.files.length > 0;

if (!validCommits && !hasFiles) {
this.logger.debug(`Skipping release ${payload.release} for project ${projectId}: no valid commits or source maps`);

return;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

add debug log about release skipping with project id

}

await this.createReleaseIfMissing(projectId, payload.release);

/**
* Save commits
*/
if (commits && this.areCommitsValid(commits)) {
if (validCommits) {
const commitsWithParsedDate: CommitData[] = commits.map(commit => ({
...commit,
date: new Date(commit.date),
Expand All @@ -106,13 +116,11 @@ export default class ReleaseWorker extends Worker {
$set: {
commits: commitsWithParsedDate,
},
}, {
upsert: true,
});
}

// save source maps
if (payload.files) {
if (hasFiles) {
await this.saveSourceMap(projectId, payload);
}
} catch (err) {
Expand All @@ -122,6 +130,54 @@ export default class ReleaseWorker extends Worker {
}
}

/**
* Create a release once and assign its first-registration sequence.
*
* The sequence starts at 1 for each project and is based on the order in
* which new releases are received by this sequential worker. Repeated
* commits or source-map uploads keep the existing sequence.
*
* @param projectId - project id to bind the corresponding release.
* @param release - release name
*/
private async createReleaseIfMissing(projectId: string, release: string): Promise<void> {
const existingRelease = await this.releasesCollection.findOne({
projectId,
release,
});

if (existingRelease) {
return;
}

const lastRelease = await this.releasesCollection.findOne({
projectId,
releaseSequence: { $exists: true },
}, {
sort: { releaseSequence: -1 },
projection: { releaseSequence: 1 },
});

const releaseSequence = (lastRelease?.releaseSequence || 0) + 1;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure it will work properly in case of several workers running in parallel.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

You are right. This code is safe only with the current deployment setup: one release worker with SIMULTANEOUS_TASKS=1.

With multiple workers, two new releases could read the same last sequence and get the same number. I kept this approach to avoid adding another MongoDB collection.

Should we keep this limitation, or should we add a small releaseSequenceCounters collection and use an atomic $inc operation? The sequence would still be stored in releases; the extra collection would only generate the next number safely.


try {
await this.releasesCollection.insertOne({
projectId,
release,
releaseSequence,
commits: [],
} as unknown as ReleaseDBScheme);
} catch (error) {
if ((error as MongoError).code?.toString() === DB_DUPLICATE_KEY_ERROR) {
this.logger.debug(`Release ${release} for project ${projectId} was created by another worker`);

return;
}

throw error;
}
}

/**
* Check commtis for the content of all required data
*
Expand Down Expand Up @@ -208,31 +264,8 @@ export default class ReleaseWorker extends Worker {

try {
/**
* - insert new record with saved maps
* or
* - update previous record with adding new saved maps
* Add new source maps to the release created by saveRelease.
*/
if (!existedRelease) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why this code has been removed?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This code was moved to createReleaseIfMissing, which runs before saveSourceMap. So saveSourceMap only updates an existing release now.

this.logger.info('trying insert new release');

try {
await this.releasesCollection.insertOne({
projectId: projectId,
release: payload.release,
files: savedFilesWithoutContent,
} as ReleaseDBScheme);
this.logger.info('inserted new release');
} catch (err) {
if ((err as MongoError).code.toString() === DB_DUPLICATE_KEY_ERROR) {
this.logger.warn(`Duplicate key on insert, retrying update after small delay`);
/* eslint-disable @typescript-eslint/no-magic-numbers */
await new Promise(resolve => setTimeout(resolve, 200));
} else {
throw err;
}
}
}

await this.releasesCollection.findOneAndUpdate({
projectId: projectId,
release: payload.release,
Expand Down
49 changes: 46 additions & 3 deletions workers/release/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ describe('Release Worker', () => {
});
db = connection.db();
collection = await db.collection<ReleaseDBScheme>('releases');
await collection.createIndex(
{
projectId: 1,
release: 1,
},
{
name: 'projectId_release_unique_idx',
unique: true,
}
);

await mockBundle.build();
});
Expand Down Expand Up @@ -184,13 +194,18 @@ describe('Release Worker', () => {
await expect(release).toMatchObject(parsedReleasePayload);
});

test('should update a release if it is already exists', async () => {
test('should keep the same release sequence on repeated uploads', async () => {
await worker.handle({
projectId,
type: 'add-release',
payload: releasePayload,
});

const firstRelease = await collection.findOne({
projectId,
release: releasePayload.release,
});

await worker.handle({
projectId,
type: 'add-release',
Expand All @@ -200,9 +215,37 @@ describe('Release Worker', () => {
},
});

const count = await collection.countDocuments();
const secondRelease = await collection.findOne({
projectId,
release: releasePayload.release,
});

await expect(secondRelease.releaseSequence).toEqual(firstRelease.releaseSequence);
await expect(await collection.countDocuments()).toEqual(1);
});

test('should assign increasing sequences to new releases', async () => {
await worker.handle({
projectId,
type: 'add-release',
payload: releasePayload,
});

await worker.handle({
projectId,
type: 'add-release',
payload: {
...releasePayload,
release: 'Dapper Dragon 2',
},
});

const releases = await collection.find({ projectId })
.sort({ releaseSequence: 1 })
.toArray();

await expect(count).toEqual(1);
await expect(releases).toHaveLength(2);
await expect(releases.map(release => release.releaseSequence)).toEqual([1, 2]);
});

test('should correctly handle release with multiple source maps in a single transaction', async () => {
Expand Down
Loading