Update member tracking on group transfer
What does this MR do and why?
It adds a new worker (GitlabSubscriptions::CleanupSubscriptionSeatAssignmentWorker) that updates the member tracking table (subscription_seat_assignments) after a group or project transfer. It does so by enqueuing the worker in the post_update_hooks of both the Groups::TransferService and Projects::TransferService. The worker itself performs the following tasks:
- finds all seat assignment records for the old root namespace
- checks the user for each record still has a membership in the namespace hierarchy
- removes any seat assignment that are no longer valid (i.e. user no longer has a membership in the hierarchy)
- verifies the new root namespace for missing seat assignments
- checks the new namespace hierarchy for members without seat assignments
- creates seats if members do not have them associated with the top namespace
- Trigger the newly created worker on group/project transfer
References
[BE] Update member tracking table on group transfer
How to set up and validate locally
Prerequisite:
- In rails console enable the feature flag
Feature.enable(:gitlab_com_subscriptions)
Test 01 (Subgroup of a root group becomes a root group)
- Create a group (G1)
- Click the
invite membersbutton. - Invite a few members of your choice and assign them the role of developers
- On the main page of G1, create a new subgroup (SG1)
- On the SG1 page click the
invite membersbutton. - Invite one or more members so that the new members of the subgroup are NOT direct members of the root group
- Using the rails console, type
GitlabSubscriptions::SeatAssignment.by_namespace(Group.find({id of G1}))to obtain the total of seats for the root group (G1). Take down the number and ids of theseat assignments - Go to SG1 main page, and on the left hand sidebar locate the settings section and click on general
- Scroll down to advance and click on it
- Locate the Transfer group section and select no parent group
- Click on Transfer group and follow the instructions
- SG1 should now be a root group
- By now, the
CleanupSubscriptionSeatAssignmentWorkerworker should be executed - Go to the rails console, type
GitlabSubscriptions::SeatAssignment.by_namespace(Group.find({id of G1})), and note the output (number of records and ids) - Compare the results of (6) with those of (13) and notice that the members invite of SG1 no longer occupies a seat assignment in G1
- Go to the rails console, type
GitlabSubscriptions::SeatAssignment.by_namespace(Group.find({id of SG1})), and note the output (number of records and ids) - Note that the seat assignments corresponds to the members of SG1 that were transferred from G1
Test 02 (Subgroup (SG2) of a root group to subgroup to another root group (G2))
- Follow steps 1-3 in TEST01 to create another root group with some members (G2)
- Using G1 created in TEST01, follow steps 4-7 to create a new subgroup (SG2)
- Using the rails console, type
GitlabSubscriptions::SeatAssignment.by_namespace(Group.find({id of G1}))to obtain the total of seats for the root group (G1). Take down the number and ids of theseat assignments - Using the rails console, type
GitlabSubscriptions::SeatAssignment.by_namespace(Group.find({id of G2}))to obtain the total of seats for the root group (G2). Take down the number and ids of theseat assignments - Go to SG2 main page, and on the left hand sidebar locate the settings section and click on general
- Scroll down to advance and click on it
- Locate the Transfer group section and select SG2. to transfer the subgroup from G1 to G2
- Click on Transfer group and follow the instructions
- SG2 should now be subgroup of G2
- Go to the rails console, type
GitlabSubscriptions::SeatAssignment.by_namespace(Group.find({id of G1})), and note the output (number of records and ids) - Compare the results of step 3 with the new results, and notice that the seat assignments for members of SG2 are no longer present.
- Go to the rails console, type
GitlabSubscriptions::SeatAssignment.by_namespace(Group.find({id of G2})), and note the output (number of records and ids) - Note that G2 now have seat assignments corresponding to the members of SG2
TEST 03 (Top group to subgroup)
- Create a new group (G3)
- Invite a few members and assign them the role of developers (if you use G1 for the transfer, add members that are not in G1)
- In the rails console type
GitlabSubscriptions::SeatAssignment.by_namespace(Group.find({id of G3}))and note the number of seats and their user ids - Go to G3 main page, and on the left hand sidebar locate the settings section and click on general
- Scroll down to advance and click on it
- Locate the Transfer group section and select G1. to transfer the root group to G1
- Click on Transfer group and follow the instructions
- G3 should now be subgroup of G1
- In the rails console type
GitlabSubscriptions::SeatAssignment.by_namespace(Namespace.find({id of G1}))and note that the members of G3 now have seat assignments in G1
TEST 04 (subgroup to subgroup in the same hierarchy)
- In G1, create a new subgroup (SG3)
- Invite a few members to SG3 (as developers). These new members must not be in the hierarchy of G1 already
- In the rails console type
GitlabSubscriptions::SeatAssignment.by_namespace(Group.find({id of G1}))and note that new members of SG3 occupy a seat assignment - Go to SG3 main page, and on the left hand sidebar locate the settings section and click on general
- Scroll down to advance and click on it
- Locate the Transfer group section and select SG1. to transfer SG3 to SG1
- Click on Transfer group and follow the instructions
- SG3 should now be a subgroup of SG1
- In the rails console type
GitlabSubscriptions::SeatAssignment.by_namespace(Group.find({id of G1})). Notice that no changes occurred with the transfer
TEST 05 (Project transfer user namespace to a group)
- Create a new project (P1) and assign a user namespace (UN) in the project URL section
- On the left hand size menu, locate manage and select members
- Click on the invite members button
- invite a new member(s) that is not a member of G1 (the destination group)
- On P1 main page, locate the settings menu, expand it, and click on general
- Scroll down to advance and click on it
- Locate the Transfer project section and select G1. to transfer P1 to G1
- Click on Transfer project and follow the instructions
- P1 should now be a project of G1
- In the rails console type
GitlabSubscriptions::SeatAssignment.by_namespace(Namespace.find({id of G1})). Notice the appropriate members of P1 now have a seat assignment in G1 - In the rails console type
GitlabSubscriptions::SeatAssignment.by_namespace(Namespace.find({id of UN})). Notice that, upon transfer, the seat assignments for members of P1 were deleted if they are no longer members of UN
TEST 06 (Project transfer from a group to a user namespace (UN))
- Create a new group, or use the same as in TEST05 (G1)
- Under G1, create a new project, or use the same as in TEST 05 (P1)
- Go to main page of P1
- On the left hand size menu, locate the manage menu and select members
- On the members page, click on the invite members button
- Invite a new member(s) that is not a member of your user namespace (UN, the destination user namespace)
- On P1 main page, locate the settings menu, expand it, and click on general
- Scroll down to advance and click on it
- Locate the Transfer project section and select UN. to transfer P1 to UN
- Click on Transfer project and follow the instructions
- P1 should now be a project of UN
- In the rails console type
GitlabSubscriptions::SeatAssignment.by_namespace(Namespace.find({id of G1})). Notice the the member that were transferred in P1 to UN and that are no longer members of G1, no longer have seat assignments - In the rails console type
GitlabSubscriptions::SeatAssignment.by_namespace(Namespace.find({id of UN})). Notice that, upon transfer, the seat assignments for members of P1 were were created in UN
TEST 07 (Project transfer from a group (G1) to another group (G2) )
- Create a new group, or use the same as in TEST06 (G1)
- Create a new destination group (G2)
- Under G1, create a new project (P1)
- Go to main page of P1
- On the left hand size menu, locate the manage menu and select members
- On the members page, click on the invite members button
- Invite a new member(s) that is not a member of G2 (the destination group)
- On P1 main page, locate the settings menu, expand it, and click on general
- Scroll down to advance and click on it
- Locate the Transfer project section and select G2. to transfer P1 to G2
- Click on Transfer project and follow the instructions
- P1 should now be a project of G2
- In the rails console type
GitlabSubscriptions::SeatAssignment.by_namespace(Namespace.find({id of G1})). Notice the the members that were transferred in P1 to UN and that are no longer members of G1, no longer have seat assignments - In the rails console type
GitlabSubscriptions::SeatAssignment.by_namespace(Namespace.find({id of G2})). Notice that, upon transfer, the seat assignments for members of P1 were were created in G2
TEST 08 (Project transfer from a group (G1) to subgroup in another hierarchy (G2) )
- Create a new group, or use the same as in TEST07 (G1)
- Create a new group (G2)
- Create a new destination subgroup (SG1) within G2
- Under G1, create a new project (P1)
- Go to main page of P1
- On the left hand size menu, locate the manage menu and select members
- On the members page, click on the invite members button
- Invite a new member(s) that is not a member of G2 (the destination root group)
- On P1 main page, locate the settings menu, expand it, and click on general
- Scroll down to advance and click on it
- Locate the Transfer project section and select SG1. to transfer P1 to SG1
- Click on Transfer project and follow the instructions
- P1 should now be a project of SG2
- In the rails console type
GitlabSubscriptions::SeatAssignment.by_namespace(Namespace.find({id of G1})). Notice that the P1 members who were transferred G1 to UN and that are no longer members of G1, no longer have seat assignments - Since G2 is the root ancestor of SG1, in the rails console type
GitlabSubscriptions::SeatAssignment.by_namespace(Namespace.find({id of G2})). Notice that, upon transfer, the seat assignments for members of P1 were were created in G2
TEST 09 (Project transfer from a group (G1) to subgroup in the same hierarchy (SG3) )
- Create a new group, or use the same as in TEST07 (G1)
- Create two new subgroups, SG1, and SG2. We will transfer a project from SG1 to SG2
- Under SG1, create a new project (P1)
- Go to main page of P1
- On the left hand size menu, locate the manage menu and select members
- On the members page, click on the invite members button
- Invite a new member(s) that is not a member of G1 (in this case, the current and the destination root group)
- In the rails console type
GitlabSubscriptions::SeatAssignment.by_namespace(Namespace.find({id of G1})). Take note of the seat assignments; these should correspond to the members of G1 and its subgroups (functionality outside the scope of this MR) - On P1 main page, locate the settings menu, expand it, and click on general
- Scroll down to advance and click on it
- Locate the Transfer project section and select SG2. to transfer P1 from SG1 to SG2
- Click on Transfer project and follow the instructions
- P1 should now be a project of SG2
- In the rails console type
GitlabSubscriptions::SeatAssignment.by_namespace(Namespace.find({id of G1})). Notice that the seat assignments of step 9 are the same after the transfer (no changes).
SQL plans
GitlabSubscriptions::CleanupSubscriptionSeatAssignmentWorker
-
remove_outdated_seat_assignments
-
outdated_seat_assignments&.delete_all
-
DELETE FROM "subscription_seat_assignments" WHERE "subscription_seat_assignments"."namespace_id" = 347 AND "subscription_seat_assignments"."user_id" != 141
-
seat_assignments.delete_all
DELETE FROM "subscription_seat_assignments" WHERE "subscription_seat_assignments"."namespace_id" = 643
-
create_new_seat_assignments
-
GitlabSubscriptions::SeatAssignment.insert_all(seat_assignments, unique_by: [:namespace_id, :user_id])
-
INSERT INTO "subscription_seat_assignments" ("namespace_id","user_id","created_at","updated_at","organization_id") VALUES (347, 141, '2025-05-13 23:20:16.800991', '2025-05-13 23:20:16.801034', 1) ON CONFLICT ("namespace_id","user_id") WHERE (namespace_id IS NOT NULL) DO NOTHING RETURNING "id"
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.