Skip to content

Task #644 Refactor create and edit assignemnts functions#20

Open
rolivares93 wants to merge 15 commits intomainfrom
task-644/refactor-create-and-edit-assignemnts-functions
Open

Task #644 Refactor create and edit assignemnts functions#20
rolivares93 wants to merge 15 commits intomainfrom
task-644/refactor-create-and-edit-assignemnts-functions

Conversation

@rolivares93
Copy link
Contributor

Proposed changes

I refactored the functions responsible for the assignment creation to avoid the delay caused by firebase background functions.

Screen.Recording.2025-11-05.at.10.53.55.mov

Types of changes

What types of changes does this pull request introduce?

  • Bugfix (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)
  • Refactor (non-breaking change that does not add functionality but makes code cleaner or more efficient)
  • Tests (new or updated tests)
  • Styles (changes to code styling)
  • CI (continuous integration changes)
  • Other (please describe below)

Additional Notes

@rolivares93 rolivares93 self-assigned this Nov 5, 2025
@Zio-4
Copy link
Contributor

Zio-4 commented Nov 5, 2025

We need a way to emulate a complete atomic transaction. In other words, we need to guarantee that all users were assigned the assignment successfully. The current approach can fail for some users, while the rest would have the assignment. We want to avoid that. This also uses the previous approach of background triggers. We do not want eventual consistency. All users should be able to log in and play the assignment immediately once the admin creates it.

@rolivares93 rolivares93 force-pushed the task-644/refactor-create-and-edit-assignemnts-functions branch from 5b98fde to 0ee73ee Compare November 12, 2025 13:41
Copy link
Contributor

@Zio-4 Zio-4 left a comment

Choose a reason for hiding this comment

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

@Zio-4
Copy link
Contributor

Zio-4 commented Nov 12, 2025

Since this is a big PR and takes time to review, I'm gonna hold off until I get a response to the comments above.

administrationData,
transaction
orgChunk,
administrationData
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you think the rollbackAssignmentCreation needs to be chunked as well since the db supports some MAX_TRANSACTIONS? If not, I suggest we just simply pass usersToUpdate since we tested that rollbackAssignmentCreation does not affect users that have not been created. So it is safe to pass userIds that have no entries.

// If remainingUsersToRemove.length === 0, then these chunks will be of zero length
// and the entire loop below is a no-op.
for (const _userChunk of _chunk(remainingUsers, MAX_TRANSACTIONS)) {
await db.runTransaction(async (transaction) => {
Copy link
Collaborator

@asengupta3 asengupta3 Dec 1, 2025

Choose a reason for hiding this comment

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

Also I am not in favor of this pattern. What is going to happen here is, we are going to do sequential updates. Meaning we will update a set of users; wait and then update the next set of users, that adds time additively. Do you think firebase supports parallel requests (at least we should try it out)? In that case what I suggest pushing all the updateAssignmentForUsers and other async functions in an array and run Promise.all(array). That way the requests will be fired together and be faster.


if (mode === "add") {
totalUsersAssigned = usersToUpdate.length;
createdUserIds.push(...usersToUpdate);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a reason we need to create a copy of usersToUpdate? Can't we simply pass that?

try {
for (let i = 0; i < orgChunks.length; i++) {
const orgChunk = orgChunks[i];
const result = await updateAssignmentsForOrgChunkHandler({
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should follow the same Promise.all pattern here as well if we have verified that it works for the updateAssignmentsForOrgChunkHandler function

});

for (const _userChunk of _chunk(remainingUsersToRemove, MAX_TRANSACTIONS)) {
await db.runTransaction(async (transaction) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same promise.all here as well


for (let i = 0; i < orgChunks.length; i++) {
const orgChunk = orgChunks[i];
const result = await updateAssignmentsForOrgChunkHandler({
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same Promise.all here as well.

if (orgChunk && administrationData && userIds.length > 0) {
try {
const { updateAdministrationStatsForOrgChunk } = await import(
"./on-assignment-updates.js"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Try to move this to the top as well

@rolivares93
Copy link
Contributor Author

CREATING ASSIGNMENT

CREATING.ASSIGNMENT.2025-Dec-17-2.mov

VIEWING ASSIGNMENT

VIEWING.ASSIGNMENT.2025-Dec-17-2.mov

Copy link
Contributor

@Zio-4 Zio-4 left a comment

Choose a reason for hiding this comment

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

It's hard to understand what a lot of the code is doing without going deep into the details like the "standardizeAdministrations" function for example.

Could you comments throughout the functions to make it clear what's happening like what the function is actually doing?

I was originally hoping we could completely refactor the create / edit assignment functions, but this will do for now.

});
}

return { success: false, userIds: createdUserIds, error };
Copy link
Contributor

Choose a reason for hiding this comment

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

If we're going to do this, then we need to update the frontend to check for this.

My take, since we're doing it atomically, all or nothing, we should just throw an error and then the frontend can say that user creation failed. Because there's no partial success, so we're not going to be showing rows of users that failed.

Comment on lines +235 to +249
if (usersToRemove.length !== 0) {
if (usersToRemove.length <= MAX_TRANSACTIONS) {
return removeOrgsFromAssignments(
usersToRemove,
[administrationId],
removedExhaustiveOrgs,
transaction
);
} else {
remainingUsersToRemove = usersToRemove;
return Promise.resolve(usersToRemove.length);
}
} else {
return Promise.resolve(0);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this section of code doing? Why are we resolving with the usersToRemove and then or resolving with zero? I think this should be rewritten to be more readable.

Comment on lines +671 to +679
if (!creatorUid) {
logger.error(
`Cannot sync assignments: administration ${newAdministrationId} has no createdBy field`,
{ administrationId: newAdministrationId }
);
throw new HttpsError(
"internal",
"Administration document is missing creator information"
);
Copy link
Contributor

Choose a reason for hiding this comment

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

This shouldn't be possible because the creatorId will always be on the auth token when we first create the administration document, so it could never be missing it.

Comment on lines +696 to +705
if (!creatorUid) {
logger.error(
`Cannot sync assignments: administration ${newAdministrationId} has no createdBy field`,
{ administrationId: newAdministrationId }
);
throw new HttpsError(
"internal",
"Administration document is missing creator information"
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Not possible

// If this is a new administration and sync failed, the error was already thrown
// and should have triggered rollback. Re-throw to prevent the function from
// returning successfully when assignments failed to be created.
const isNewAdministration = !administrationId;
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's move this variable up a scope if we're going to reuse it here so we don't re-declare the same variables.

@Zio-4
Copy link
Contributor

Zio-4 commented Dec 17, 2025

Also important issues that Codex caught:

  • [P1] Rollback batch reused after commit fails beyond 500 users — assignments/assignment-utils.ts:204-214
    add writes to a batch that has been committed, aborting the rollback and leaving many assignments/stat counters in place. To ensure rollback works for large cohorts, a new batch should be created each time a commit occurs.
  • [P1] Partial assignments not rolled back when org chunk transaction fails — /assignments/sync-assignments.ts:226-291
    The new synchronous handler executes user chunks in Promise.all and only populates createdUserIds after all chunk transactions resolve. If any transaction rejects, Promise.all throws before that push runs, yet earlier transactions may already have succeeded; the catch path treats createdUserIds as empty and skips rollback. The caller then reports the chunk as failed but leaves successfully written assignments in place, breaking the advertised atomicity for new administrations. Track successes per chunk and roll them back on error.

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.

3 participants