Services running propagations

file service groups ancestors items ancestors permissions results
app/api/answers/submit.go itemGetAnswerToken     sync (if at least one item is unlocked by the results propagation) sync
app/api/auth/create_access_token.go accessTokenCreate sync (if the code is given)   sync (if new badges are loaded and ‘group_membership’ permissions are removed or at least one item is unlocked by the results propagation) sync (if new badges are loaded)
app/api/auth/create_temp_user.go tempUserCreate sync      
app/api/contests/set_additional_time.go contestSetAdditionalTime sync (if groups_groups.expires_at is changed)   sync (if groups_groups.expires_at is changed and at least one item is unlocked by the results propagation) sync (if groups_groups.expires_at is changed)
app/api/currentuser/accept_group_invitation.go groupInvitationAccept sync (if the group is not a team)   sync (if at least one item is unlocked by the results propagation) sync (if the group is not a team)
app/api/currentuser/create_group_join_request.go groupJoinRequestCreate sync (if the request is automatically accepted and the group is not a team)   sync (if at least one item is unlocked by the results propagation) sync (if the request is automatically accepted and the group is not a team)
app/api/currentuser/delete.go currentUserDeletion     sync (if at least one permissions_granted with source_group_id=user.group_id is removed)  
app/api/currentuser/join_group_by_code.go groupsJoinByCode sync (if the group is not a team)   sync (if at least one item is unlocked by the results propagation) sync (if the group is not a team)
app/api/currentuser/leave_group.go groupLeave sync (if the group is not a team)   sync (if ‘group_membership’ permissions are removed)  
app/api/currentuser/refresh.go userDataRefresh sync (if new badges are loaded)   sync (if new badges are loaded and ‘group_membership’ permissions are removed or at least one item is unlocked by the results propagation) sync (if new badges are loaded)
app/api/groups/accept_join_requests.go groupJoinRequestsAccept sync (if there are join requests to accept and the group is not a team)   sync (if at least one item is unlocked by the results propagation) sync (if there are join requests to accept and the group is not a team)
app/api/groups/accept_leave_requests.go groupLeaveRequestsAccept sync (if there are leave requests to accept and the group is not a team)   sync (if there are leave requests to accept and ‘group_membership’ permissions are removed)  
app/api/groups/add_child.go groupAddChild sync   sync (if at least one item is unlocked by the results propagation) sync
app/api/groups/create_group.go groupCreate sync      
app/api/groups/create_invitations.go groupInvitationsCreate sync (if at least one join request is automatically accepted and the group is not a team)   sync (if at least one item is unlocked by the results propagation) sync (if at least one join request is automatically accepted)
app/api/groups/create_user_batch.go createUserBatch sync      
app/api/groups/delete_group.go groupDelete sync (if at least one group relation is deleted)   sync (if at least one permissions_granted linked to a removed group via source_group_id is deleted)  
app/api/groups/remove_child.go groupRemoveChild sync (if at least one group relation is deleted)   sync (if at least one permissions_granted linked to a removed group via source_group_id is deleted)  
app/api/groups/remove_members.go groupMembersRemove sync (if at least one member is removed)   sync (if at least one member is removed and ‘group_membership’ permissions are removed)  
app/api/groups/remove_user_batch.go userBatchRemove     sync (if at least one permissions_granted with source_group_id=user.group_id is removed)  
app/api/groups/update_group.go groupUpdate sync (if group members are removed because of approval rules strengthening and the group is not a team)   sync (if group members are removed because of approval rules strengthening and ‘group_membership’ permissions are removed)  
app/api/groups/update_permissions.go updatePermissions     sync sync (if ‘can_view’ or ‘is_owner’ is changed)
app/api/items/apply_dependency.go itemDependencyApply     sync (if at least one item is unlocked) sync (if at least one item is unlocked)
app/api/items/ask_hint.go itemGetHintToken     sync (if at least one item is unlocked by the results propagation) sync
app/api/items/create_attempt.go attemptCreate     sync (if at least one item is unlocked by the results propagation) sync
app/api/items/create_item.go itemCreate   sync (if at least one items_items row is created) async* async*
app/api/items/delete_item.go itemDelete   sync sync sync
app/api/items/end_attempt.go itemAttemptEnd sync      
app/api/items/enter.go itemEnter sync (if items.participants_group_id is not null)   sync (if items.participants_group_id is not null and at least one item is unlocked by the results propagation) sync (if items.participants_group_id is not null)
app/api/items/generate_task_token.go itemTaskTokenGenerate     sync (if at least one item is unlocked by the results propagation) sync
app/api/items/save_grade.go saveGrade     sync (if at least one item is unlocked by the results propagation) sync
app/api/items/start_result.go resultStart       async* (if the result is inserted or updated)
app/api/items/start_result_path.go resultStartPath       async* (if the result are to be inserted)
app/api/items/update_item.go itemUpdate   sync (if children are modified) async* (if children are modified) async* (if children/no_score/validation_type are modified)

async* works as follows:

  1. It tries to schedule an async propagation of the specified kind by calling a special HTTP endpoint.
  2. If the endpoint succeeds, the specified propagation really runs async.
  3. If the endpoint fails (due to a network error, a 3-second timeout, or a non-200 HTTP response), no matter which of propagations it tried to schedule, it synchronously runs the sequence of three propagations:
    • permissions,
    • results.

🤷

Notes

Objects marked for propagations are shared between all DB sessions

As of April 2024, since, instead of running sync permissions/results propagations in the same transaction, we run them in separate transactions, objects marked for propagations are shared between all DB sessions. This means that if a sync propagation is run in one DB session, it processes all objects marked for this propagation, no matter which DB session marked them. So, when an endpoint runs a sync permissions/results propagation to recompute some objects, it also recomputes all objects currently marked as modified by other endpoints/users. This can lead to surprisingly long propagation times even for small changes. 🤷

Impossibility of running sync and async results propagations simultaneously

It’s easy to see that there are many endpoints that run sync results propagations. At the same time, there are some endpoints that run async results propagations. As the results propagation is run under a named lock, it’s possible that a sync results propagation is waiting for an async results propagation to finish completely. This can lead to lock wait timeouts resulting in a server error response. 🤷

Happily, we have a solution for this: we can completely disable sync results propagations (with a special config setting) and run only async results propagations. This is the only way to avoid lock wait timeouts.