Skip to content

program-update-fix#608

Open
MallanagoudaB wants to merge 2 commits intodevelopfrom
ProgramUpdateFix
Open

program-update-fix#608
MallanagoudaB wants to merge 2 commits intodevelopfrom
ProgramUpdateFix

Conversation

@MallanagoudaB
Copy link
Collaborator

@MallanagoudaB MallanagoudaB commented Oct 17, 2025

Description

These recommendations are intended to promote code quality and team communication during software development. They cover a variety of topics, including ensuring that pull requests are submitted to the correct branch, documenting new methods, preserving consistency across many services, and avoiding typical blunders like accessing APIs or DB queries within loops. Sensitive data should not be uploaded, and changes to environment variables or database models should be executed consistently. Teams may work more effectively and develop higher-quality software by adhering to these standards.

Type of change

Please choose appropriate options.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Enhancement (additive changes to improve performance)
  • This change requires a documentation update

Checklist

  • It's critical to avoid making needless file modifications in contributions, such as adding new lines, console logs, or additional spaces, to guarantee cleaner and more efficient code. Furthermore, eliminating unnecessary imports from a file might enhance code readability and efficiency.
  • Ensure that the pull request is assigned to the right base branch and that the development branch name contains the JIRA Task Id. Furthermore, each commit message should include the JIRA Task Id in the manner "ED-100: message".
  • Only update packages if it is mentioned and authorized in the design document, and make sure that you have the required permissions.
  • Avoid making API and database queries inside a loop as it can lead to performance issues and slow down the system.
  • When calling another function inside a given function, add comments explaining the purpose and meaning of the passed arguments and expected return values.
  • If adding a blank argument in a function, add a comment explaining the reason for the blank argument.
  • Before submitting a pull request, do a self-review of your code to ensure there are no conflicts with the base branch and all comments have been addressed.
  • Before merging a pull request, it's important to have other team members review it to catch any potential errors or issues
  • To maintain code integrity, it's important to remove all related changes when removing code during a code review.
  • If new constants, endpoints, or utility functions are introduced, it is important to check if they already exist in the service to avoid any duplication.
  • Whenever a new environment variable is added to a service, it's important to ensure that the necessary changes are made to related files such as ".env.sample" and "envVariables.js" to maintain consistency and avoid errors. Additionally, the new environment variable should be added to the devops repository to ensure that it is properly documented and accessible to the team.
  • When adding a new function to a service, it is important to document it with relevant information such as the name, parameters, and return value in a consistent format across all services. Additionally, if there are any changes to the API response, ensure that the documentation in the controllers is updated accordingly.
  • Write a clear and concise commit message that describes the changes made.
  • Maintain consistent function signature and code across all services when adding a function to multiple services. Implement changes to database models in all services that use the same model.
  • Use only let and const. Do not use var.
  • Make common functions for repetitive code blocks.
  • Avoid uploading sensitive information such as secret tokens or passwords in pull requests to ensure data security.
  • Maintain consistent indentation and spacing throughout the code.

Summary by CodeRabbit

  • Refactor
    • Added per-program locking mechanism to prevent concurrent update conflicts.
    • Consolidated separate program update paths into unified operations.
    • Restructured component addition workflow to use safer read-then-update pattern.

@coderabbitai
Copy link

coderabbitai bot commented Oct 17, 2025

Walkthrough

The PR introduces a program-level locking mechanism and refactors component update operations across multiple helpers. It adds a new updateOne method to the Programs database class, implements a cooperative lock system keyed by programId, and updates component addition flows to perform atomic read-then-update operations within the lock.

Changes

Cohort / File(s) Change Summary
Lock mechanism
generics/helpers/utils.js
Introduces private Map programLocks and new async function executeWithProgramLock(programId, fn) that manages per-program locks, allowing functions to execute atomically within a lock. Exports the function publicly.
Database query method
databaseQueries/programs.js
Adds new public static async method updateOne(filterData = {}, updateData = {}, options = {}) on Programs class that constructs a query object and invokes the underlying database update operation with error handling.
Component update refactors
generics/helpers/solutionAndProjectTemplateUtils.js, module/project/templates/helper.js
Refactors component addition from separate external/internal paths to unified read-then-update pattern. Fetches program document (components field only) via programQueries.programsDocument, derives component order, and updates program using programQueries.updateOne with `$addToSet`. In solutionAndProjectTemplateUtils.js, wraps operation in executeWithProgramLock; in helper.js, maintains parallel OBSERVATION and SURVEY branches with conditional guards.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant executeWithProgramLock
    participant programLocks Map
    participant programQueries
    participant DB

    Caller->>executeWithProgramLock: executeWithProgramLock(programId, asyncFn)
    executeWithProgramLock->>programLocks: check for existing lock
    alt Lock exists
        executeWithProgramLock->>programLocks: wait for lock release
    end
    executeWithProgramLock->>programLocks: acquire lock
    executeWithProgramLock->>programQueries: programsDocument (fetch components)
    programQueries->>DB: query with projection
    DB-->>programQueries: program data
    programQueries-->>executeWithProgramLock: components array
    executeWithProgramLock->>Caller: execute asyncFn (calculate order, prepare update)
    Caller->>programQueries: updateOne(filterData, updateData)
    programQueries->>DB: atomic update with $addToSet
    DB-->>programQueries: result
    programQueries-->>Caller: update result
    Caller-->>executeWithProgramLock: return
    executeWithProgramLock->>programLocks: release lock
    executeWithProgramLock-->>Caller: return result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

The changes introduce new concurrent-access logic (locking mechanism) and refactor multiple update patterns across different files. While the lock implementation and database method are straightforward, the refactored component update flows require understanding the atomic read-then-update semantics and verifying correctness across two distinct code paths that follow a consistent pattern.

Possibly related PRs

  • ELEVATE-Project/project-service#547: Modifies program document updates to use updateOne method and introduce ordered component objects in similar update paths.
  • ELEVATE-Project/project-service#570: Refactors program read/update flows in the same helper files (solutionAndProjectTemplateUtils.js and module/project/templates/helper.js), replacing prior patterns with programQueries.* calls.

Suggested reviewers

  • VISHNUDAS-tunerlabs
  • praveenKDass

Poem

🔒 Locks upon programs, ordered and tight,
Each component waits for its turn in the night,
No racing ahead, no tangled affairs,
Just atomic grace and cooperative cares! 🐰✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The PR description consists entirely of the generic template without any actual content specific to this pull request. There is no explanation of what problem is being solved, why the locking mechanism was introduced, what the implementation does, or how to test the changes. All checklist items remain unchecked, and there are no details about the new updateOne method, the program-wide locking mechanism, or the refactoring of the update patterns across multiple files. This is a verbatim copy of the template rather than a meaningful description of the actual changes. Fill in the actual PR description with specific details about this changeset: explain the problem being solved (concurrent program updates), describe the solution (the new locking mechanism and its implementation), list the files affected and why they were changed, specify which checklist items apply to this PR, and provide testing guidance. The description should help reviewers understand the rationale and scope of the changes without referring to external documentation.
Title Check ❓ Inconclusive The title "program-update-fix" is vague and uses non-descriptive terminology that doesn't convey the actual purpose of the changeset. Based on the raw summary, the main objectives are to introduce a program-wide locking mechanism (executeWithProgramLock) to prevent concurrent updates and refactor multiple files to use atomic-like update patterns. The term "fix" is ambiguous—it doesn't clarify whether this addresses a bug or implements new concurrency control. A reviewer scanning commit history would not understand that the core change is introducing a locking mechanism for program updates. The title fails to communicate the specific and meaningful nature of the changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ProgramUpdateFix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@MallanagoudaB
Copy link
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Oct 17, 2025

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
module/project/templates/helper.js (1)

1292-1304: Use programExternalId from programDetails instead of programId ObjectId at line 1301.

The concern is valid and substantiated. At line 1301, programExternalId is being set to newProjectTemplateTask.programId, which is an ObjectId (confirmed in models/solutions.js). However, programExternalId must be a String (confirmed in models/solutions.js:30 and verified at line 1028 where it's populated from programDetails.programExternalId).

Fix: Change line 1301 from:

programExternalId: newProjectTemplateTask.programId,

to:

programExternalId: newProjectTemplateTask.programExternalId,

The field is available on newProjectTemplateTask (line 1028) and contains the correct String value.

🧹 Nitpick comments (1)
generics/helpers/utils.js (1)

1013-1035: Replace busy-wait lock with a fair async queue; consider timeout and multi-process note.

Current while+sleep polling is wasteful and can pile up timers under load. Queueing promises per programId serializes work without polling and is fair.

-async function executeWithProgramLock(programId, fn) {
-	// Wait until any existing lock for this program ID is released
-	while (programLocks.get(programId)) {
-		await new Promise((res) => setTimeout(res, 50)) // wait for previous update
-	}
-	programLocks.set(programId, true)
-	try {
-		return await fn()
-	} finally {
-		programLocks.delete(programId)
-	}
-}
+async function executeWithProgramLock(programId, fn) {
+	const prev = programLocks.get(programId) || Promise.resolve()
+	const run = prev.then(fn, fn)
+	// keep tail promise to maintain the chain
+	programLocks.set(
+		programId,
+		run.finally(() => {
+			if (programLocks.get(programId) === run) programLocks.delete(programId)
+		})
+	)
+	return run
+}

Note: This is in‑memory only; it won’t serialize across processes/pods. If cross‑instance serialization is required, consider a distributed lock (Mongo unique “lease” doc, Redis SET NX PX, etc.).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 07ce264 and e61af57.

📒 Files selected for processing (4)
  • databaseQueries/programs.js (1 hunks)
  • generics/helpers/solutionAndProjectTemplateUtils.js (1 hunks)
  • generics/helpers/utils.js (3 hunks)
  • module/project/templates/helper.js (2 hunks)
🧰 Additional context used
🪛 ESLint
generics/helpers/solutionAndProjectTemplateUtils.js

[error] 141-141: 'UTILS' is not defined.

(no-undef)


[error] 145-145: 'updateResult' is assigned a value but never used.

(no-unused-vars)

databaseQueries/programs.js

[error] 90-98: Unnecessary try/catch wrapper.

(no-useless-catch)


[error] 93-93: 'database' is not defined.

(no-undef)

Comment on lines +80 to +99
/**
* update one program document.
* @method
* @name updateOne
* @param {Object} [filterData={}] - Filter criteria to match the program document.
* @param {Object} [updateData={}] - Fields to update in the program document.
* @param {Object} [options={}] - Optional settings for the update operation (e.g., upsert, new).
* @returns {Object} Update result.
*/
static async updateOne(filterData = {}, updateData = {}, options = {}) {
try {
const queryObject = filterData !== 'all' ? filterData : {}

const result = await database.models.programs.updateOne(queryObject, updateData, options)

return result
} catch (error) {
throw error
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Harden updateOne: prevent empty-filter writes; remove useless catch; fix docs/options.

  • Calling updateOne with filterData 'all' or {} risks updating an arbitrary document. Block empty filters.
  • The try/catch just rethrows (no-useless-catch).
  • JSDoc mentions option 'new' which doesn’t apply to updateOne; return type is a write result, not the updated doc.
  • ESLint no-undef for database: ensure the global is available at runtime or import it consistently.

Apply this minimal fix:

-	static async updateOne(filterData = {}, updateData = {}, options = {}) {
-		try {
-			const queryObject = filterData !== 'all' ? filterData : {}
-
-			const result = await database.models.programs.updateOne(queryObject, updateData, options)
-
-			return result
-		} catch (error) {
-			throw error
-		}
-	}
+	static async updateOne(filterData = {}, updateData = {}, options = {}) {
+		const queryObject = filterData !== 'all' ? filterData : {}
+		if (Object.keys(queryObject).length === 0) {
+			throw new Error('Refusing to call updateOne() with an empty filter.')
+		}
+		return database.models.programs.updateOne(queryObject, updateData, options)
+	}

Also update the JSDoc to remove 'new' and clarify that this returns the Mongo write result. If callers need the updated document, prefer findOneAndUpdate with { new: true, lean: true }.

Run a quick grep to ensure no call site relies on updateOne returning a document:


🏁 Script executed:

#!/bin/bash
rg -nP --type=js -C2 '\bprogramQueries\.updateOne\('

Length of output: 1153


Harden updateOne: prevent empty-filter writes; remove useless catch; fix JSDoc.

Verified that all concerns are valid:

  • Empty filter {} on updateOne matches the first document in the collection, risking arbitrary updates. The code must block this.
  • The try/catch block only rethrows—ESLint no-useless-catch applies.
  • JSDoc incorrectly mentions option 'new'; MongoDB's updateOne returns a WriteResult object, not the updated document. The 'new' option applies only to findOneAndUpdate.
  • No call site uses the return value (line 145 in solutionAndProjectTemplateUtils.js assigns to updateResult but never references it; the other two call sites don't assign at all). The proposed fix is safe.

Apply the suggested diff to databaseQueries/programs.js lines 80–99:

  • Add validation to reject empty filters
  • Remove the useless try/catch block
  • Update JSDoc to reflect actual return type (WriteResult metadata)
🧰 Tools
🪛 ESLint

[error] 90-98: Unnecessary try/catch wrapper.

(no-useless-catch)


[error] 93-93: 'database' is not defined.

(no-undef)

🤖 Prompt for AI Agents
In databaseQueries/programs.js around lines 80–99, update the updateOne method
to (1) validate the incoming filterData and refuse empty filters (e.g., reject
{} or undefined unless explicitly allowed) by throwing a clear error to prevent
accidental multi-doc/first-doc updates, (2) remove the useless try/catch that
only rethrows, and (3) update the JSDoc to remove the incorrect 'new' option and
state the method returns MongoDB write-result metadata (not the updated
document); keep calling database.models.programs.updateOne(queryObject,
updateData, options) after the validation.

Comment on lines +141 to 150
UTILS.executeWithProgramLock(programData[0]._id.toString(), async () => {
const newprogramData = await programQueries.programsDocument(programMatchQuery, ['components'])

const order = (newprogramData[0].components?.length || 0) + 1
const updateResult = await programQueries.updateOne(programMatchQuery, {
$addToSet: {
components: { _id: solutionCreation._id, order },
},
})
})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Missing await means the lock doesn’t serialize; drop unused variable; ensure UTILS is in scope.

  • UTILS.executeWithProgramLock is async but not awaited, so the createSolution flow proceeds without waiting. This defeats the serialization goal and drops errors.
  • updateResult is assigned but unused.
  • ESLint: UTILS no-undef. If UTILS isn’t a guaranteed global here, import it.

Apply:

-			UTILS.executeWithProgramLock(programData[0]._id.toString(), async () => {
+			await UTILS.executeWithProgramLock(programData[0]._id.toString(), async () => {
 				const newprogramData = await programQueries.programsDocument(programMatchQuery, ['components'])
 
 				const order = (newprogramData[0].components?.length || 0) + 1
-				const updateResult = await programQueries.updateOne(programMatchQuery, {
+				await programQueries.updateOne(programMatchQuery, {
 					$addToSet: {
 						components: { _id: solutionCreation._id, order },
 					},
 				})
 			})

If UTILS isn’t globally injected, add at top:

+const UTILS = require(GENERICS_FILES_PATH + '/helpers/utils')

Use this to confirm there are no other un-awaited calls:


🏁 Script executed:

#!/bin/bash
rg -nP --type=js 'executeWithProgramLock\s*\(' -C2

Length of output: 787



---

🏁 Script executed:

```shell
#!/bin/bash
# Check if UTILS is imported at the top of solutionAndProjectTemplateUtils.js
head -50 generics/helpers/solutionAndProjectTemplateUtils.js | grep -nP '(UTILS|require|import).*UTILS'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check the broader context around the executeWithProgramLock call and updateResult usage
sed -n '135,160p' generics/helpers/solutionAndProjectTemplateUtils.js

Length of output: 939



---

🏁 Script executed:

```shell
#!/bin/bash
# Search entire file for UTILS definition/import
rg -nP 'UTILS\s*[=:]|require.*UTILS|import.*UTILS' generics/helpers/solutionAndProjectTemplateUtils.js

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check file start and structure (first 20 lines for module context)
head -20 generics/helpers/solutionAndProjectTemplateUtils.js

Length of output: 746


🏁 Script executed:

#!/bin/bash
# Check if updateResult is referenced anywhere after line 150 in the function
sed -n '141,200p' generics/helpers/solutionAndProjectTemplateUtils.js | grep -n 'updateResult'

Length of output: 78


Fix missing await, remove unused variable, and verify UTILS import.

The review findings are confirmed:

  • executeWithProgramLock is async (defined in utils.js line 1024) but not awaited at line 141, breaking serialization and error handling.
  • updateResult is assigned but never used—confirmed by search showing only one reference.
  • UTILS is not imported in solutionAndProjectTemplateUtils.js but used multiple times, indicating a missing import or reliance on global injection.

Apply the suggested fixes:

  • Add await before UTILS.executeWithProgramLock
  • Remove const updateResult = assignment, keeping only await programQueries.updateOne(...)
  • Verify or add the UTILS import if not globally injected
🧰 Tools
🪛 ESLint

[error] 141-141: 'UTILS' is not defined.

(no-undef)


[error] 145-145: 'updateResult' is assigned a value but never used.

(no-unused-vars)

🤖 Prompt for AI Agents
In generics/helpers/solutionAndProjectTemplateUtils.js around lines 141 to 150,
the call to UTILS.executeWithProgramLock is not awaited (breaking
serialization/error propagation), updateResult is assigned but never used, and
UTILS appears not to be imported; fix by adding await before
UTILS.executeWithProgramLock, change the update call to await
programQueries.updateOne(...) (remove the unused const updateResult =), and
ensure UTILS is properly imported at the top of the file (or confirm it’s
intentionally provided globally) so the symbol is defined.

Comment on lines 1331 to +1355
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Serialize OBSERVATION component append; avoid race on order.

Two requests can compute the same length and write identical order values. Wrap read+update in a program lock and await it.

-				let observationPrograms = await programQueries.programsDocument(
+				await UTILS.executeWithProgramLock(newProjectTemplateTask.programId.toString(), async () => {
+				let observationPrograms = await programQueries.programsDocument(
 					{
 						_id: newProjectTemplateTask.programId,
 					},
 					['components']
 				)
 
 				let observationComponentOrder =
 					observationPrograms && observationPrograms.length > 0
 						? observationPrograms[0]?.components?.length || 0
 						: 0
-				if (observationPrograms) {
-					await programQueries.updateOne(
+				if (observationPrograms) {
+					await programQueries.updateOne(
 						{
 							_id: newProjectTemplateTask.programId,
 							'components._id': { $ne: newProjectTemplateTask.solutionDetails._id },
 						},
 						{
 							$addToSet: {
 								components: {
 									_id: newProjectTemplateTask.solutionDetails._id,
 									order: ++observationComponentOrder,
 								},
 							},
 						}
 					)
-				}
+				}
+				})

Optional: eliminate read/compute by using a MongoDB aggregation‑pipeline update to atomically append with order: {$add: [{$size:"$components"}, 1]}. I can provide a patch if desired.


I'll verify the review comment by examining the codebase to confirm the race condition exists and validate the suggested fix.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find the helper.js file and inspect lines around 1330-1355
fd -e js 'helper\.js' | grep -i project | head -5

Length of output: 138


🏁 Script executed:

#!/bin/bash
# Search for UTILS.executeWithProgramLock to verify it exists
rg -n 'executeWithProgramLock' --type js

Length of output: 365


🏁 Script executed:

#!/bin/bash
# Search for programQueries.updateOne to understand the update behavior
rg -nA 5 'updateOne' --type js -C 2 | head -50

Length of output: 3708


🏁 Script executed:

#!/bin/bash
# Search for other uses of program locking patterns
rg -n '\$ne|addToSet' --type js -C 3 | head -100

Length of output: 6115



---

🏁 Script executed:

```shell
#!/bin/bash
# Inspect the actual code at lines 1320-1365 to see current state
sed -n '1320,1365p' module/project/templates/helper.js

Length of output: 1425


🏁 Script executed:

#!/bin/bash
# Check if executeWithProgramLock is imported/used in this helper.js
rg -n 'executeWithProgramLock|import.*UTILS' module/project/templates/helper.js -A 2 -B 2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check the survey component update block around line 1398
sed -n '1390,1420p' module/project/templates/helper.js

Length of output: 740



---

🏁 Script executed:

```shell
#!/bin/bash
# Check the beginning of the file for imports and UTILS availability
head -100 module/project/templates/helper.js | grep -n "UTILS\|require\|import"

Length of output: 1801


🏁 Script executed:

#!/bin/bash
# Check all UTILS references in this file
rg -n 'UTILS\.' module/project/templates/helper.js | head -20

Length of output: 1650


🏁 Script executed:

#!/bin/bash
# Get context around the observation block to understand function scope
sed -n '1300,1330p' module/project/templates/helper.js

Length of output: 980


🏁 Script executed:

#!/bin/bash
# Check if survey block also needs same fix
sed -n '1380,1410p' module/project/templates/helper.js

Length of output: 923



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for UTILS constant definition/export
rg -n 'exports\.UTILS|UTILS\s*=|global\.UTILS' --type js | head -10

Length of output: 62


🏁 Script executed:

#!/bin/bash
# Check the app initialization to understand UTILS global setup
rg -n 'global\.UTILS\s*=|require.*utils' generics/helpers/utils.js | head -5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Verify the structure: check line numbers before and after survey to confirm it's same pattern
sed -n '1375,1415p' module/project/templates/helper.js | cat -n

Length of output: 1527


Wrap read+update in program lock for both observation AND survey component appends; avoid race on order.

Two requests can compute the same component length and write identical order values. The observation block (lines 1330–1355) and the survey block (lines 1388–1410) both have this vulnerability. Wrap each read+update in UTILS.executeWithProgramLock():

+				await UTILS.executeWithProgramLock(newProjectTemplateTask.programId.toString(), async () => {
 				let observationPrograms = await programQueries.programsDocument(
 					{
 						_id: newProjectTemplateTask.programId,
 					},
 					['components']
 				)

 				let observationComponentOrder =
 					observationPrograms && observationPrograms.length > 0
 						? observationPrograms[0]?.components?.length || 0
 						: 0
 				if (observationPrograms) {
 					await programQueries.updateOne(
 						{
 							_id: newProjectTemplateTask.programId,
 							'components._id': { $ne: newProjectTemplateTask.solutionDetails._id },
 						},
 						{
 							$addToSet: {
 								components: {
 									_id: newProjectTemplateTask.solutionDetails._id,
 									order: ++observationComponentOrder,
 								},
 							},
 						}
 					)
 				}
+				})

Apply the same lock wrapping to the survey block (lines ~1388–1410).

Optional: eliminate read/compute by using MongoDB aggregation-pipeline update with order: {$add: [{$size:"$components"}, 1]} for atomic append without locks.

📝 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
{
_id: newProjectTemplateTask.programId,
},
{
$addToSet: {
components: {
_id: newProjectTemplateTask.solutionDetails._id,
order: ++nextComponentOrder,
},
},
}
['components']
)
let observationComponentOrder =
observationPrograms && observationPrograms.length > 0
? observationPrograms[0]?.components?.length || 0
: 0
if (observationPrograms) {
await programQueries.updateOne(
{
_id: newProjectTemplateTask.programId,
'components._id': { $ne: newProjectTemplateTask.solutionDetails._id },
},
{
$addToSet: {
components: {
_id: newProjectTemplateTask.solutionDetails._id,
order: ++observationComponentOrder,
},
},
}
)
}
await UTILS.executeWithProgramLock(newProjectTemplateTask.programId.toString(), async () => {
let observationPrograms = await programQueries.programsDocument(
{
_id: newProjectTemplateTask.programId,
},
['components']
)
let observationComponentOrder =
observationPrograms && observationPrograms.length > 0
? observationPrograms[0]?.components?.length || 0
: 0
if (observationPrograms) {
await programQueries.updateOne(
{
_id: newProjectTemplateTask.programId,
'components._id': { $ne: newProjectTemplateTask.solutionDetails._id },
},
{
$addToSet: {
components: {
_id: newProjectTemplateTask.solutionDetails._id,
order: ++observationComponentOrder,
},
},
}
)
}
})
🤖 Prompt for AI Agents
In module/project/templates/helper.js around lines 1330–1355, the observation
component read+update sequence must be wrapped in UTILS.executeWithProgramLock
to prevent race conditions when computing and writing the component order;
replace the current read (fetching observationPrograms and computing
observationComponentOrder) and subsequent updateOne call with a single
UTILS.executeWithProgramLock(async () => { /* perform the read and update inside
the lock using newProjectTemplateTask.programId as the lock key */ }); and apply
the identical lock wrapping to the survey append block around lines ~1388–1410
so both blocks use the program lock; alternatively you may replace read+update
with an aggregation-pipeline update that sets order: {$add: [{$size:
"$components"}, 1]} for an atomic append without locks.

Comment on lines 1376 to +1412
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Same race in SURVEY path; wrap with the lock (and consider atomic pipeline update).

Mirror the OBSERVATION fix here:

-				let surveyPrograms = await programQueries.programsDocument(
+				await UTILS.executeWithProgramLock(newProjectTemplateTask.programId.toString(), async () => {
+				let surveyPrograms = await programQueries.programsDocument(
 					{
 						_id: newProjectTemplateTask.programId,
 					},
 					['components']
 				)
 
 				let surveyComponentOrder =
 					surveyPrograms && surveyPrograms.length > 0 ? surveyPrograms[0]?.components?.length || 0 : 0
 
-				if (surveyPrograms) {
-					await programQueries.updateOne(
+				if (surveyPrograms) {
+					await programQueries.updateOne(
 						{
 							_id: newProjectTemplateTask.programId,
 							'components._id': { $ne: newProjectTemplateTask.solutionDetails._id },
 						},
 						{
 							$addToSet: {
 								components: {
 									_id: newProjectTemplateTask.solutionDetails._id,
 									order: ++surveyComponentOrder,
 								},
 							},
 						}
 					)
-				}
+				}
+				})
📝 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
{
_id: newProjectTemplateTask.programId,
},
{
$addToSet: {
components: {
_id: newProjectTemplateTask.solutionDetails._id,
order: ++nextComponentOrder,
},
},
}
['components']
)
let surveyComponentOrder =
surveyPrograms && surveyPrograms.length > 0 ? surveyPrograms[0]?.components?.length || 0 : 0
if (surveyPrograms) {
await programQueries.updateOne(
{
_id: newProjectTemplateTask.programId,
'components._id': { $ne: newProjectTemplateTask.solutionDetails._id },
},
{
$addToSet: {
components: {
_id: newProjectTemplateTask.solutionDetails._id,
order: ++surveyComponentOrder,
},
},
}
)
}
await UTILS.executeWithProgramLock(newProjectTemplateTask.programId.toString(), async () => {
let surveyPrograms = await programQueries.programsDocument(
{
_id: newProjectTemplateTask.programId,
},
['components']
)
let surveyComponentOrder =
surveyPrograms && surveyPrograms.length > 0 ? surveyPrograms[0]?.components?.length || 0 : 0
if (surveyPrograms) {
await programQueries.updateOne(
{
_id: newProjectTemplateTask.programId,
'components._id': { $ne: newProjectTemplateTask.solutionDetails._id },
},
{
$addToSet: {
components: {
_id: newProjectTemplateTask.solutionDetails._id,
order: ++surveyComponentOrder,
},
},
}
)
}
})
🤖 Prompt for AI Agents
In module/project/templates/helper.js around lines 1388 to 1412, the SURVEY
branch suffers the same race as the OBSERVATION code: reading surveyPrograms to
compute surveyComponentOrder and then doing an update is not protected and can
create duplicate/incorrect orders under concurrency. Wrap the read+update in the
same lock used for OBSERVATION (acquire lock on programId, re-fetch components
inside the lock), or replace with a single atomic update pipeline (e.g., use a
findOneAndUpdate with an aggregation pipeline that computes length/next order
and $addToSet if not present) so the check and insert are atomic; release the
lock after the update.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments