Skip to content

Commit 5310aca

Browse files
authored
Fix chunk iteration for user migration script (#4378)
1 parent ba4fe8d commit 5310aca

File tree

2 files changed

+33
-13
lines changed

2 files changed

+33
-13
lines changed

keycloak_user_export/management/commands/migrate_users_to_keycloak.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import requests
99

1010
from keycloak_user_export.models import UserExportToKeycloak
11-
from open_discussions.utils import chunks
1211

1312
User = get_user_model()
1413

@@ -221,13 +220,12 @@ def handle(self, *args, **kwargs):
221220
social_auth__provider=kwargs["filter_provider_name"]
222221
)
223222
unsynced_users = (
224-
User.objects.only("email")
223+
User.objects.only("email", "date_joined")
225224
.filter(is_active=True)
226225
.exclude(social_auth__provider="ol-oidc")
227226
.exclude(userexporttokeycloak__isnull=False)
228227
.filter(unsynced_users_social_auth_query)
229-
.select_related("userexporttokeycloak")
230-
.prefetch_related("social_auth")
228+
.prefetch_related("profile")
231229
)
232230

233231
with requests.Session() as session:
@@ -239,7 +237,9 @@ def handle(self, *args, **kwargs):
239237

240238
# Process batches of the users who must be exported.
241239
batch_size = kwargs["batch_size"]
242-
for batch in chunks(unsynced_users, chunk_size=batch_size):
240+
# the users are updated in such a way that they're excluded from this query the next time
241+
# so we just keep grabbing a slice from the beginning of the query until it returns empty
242+
while batch := unsynced_users[:batch_size]:
243243
payload = {
244244
"ifResourceExists": "SKIP",
245245
"realm": settings.KEYCLOAK_REALM_NAME,

keycloak_user_export/management/commands/tests/test_migrate_users_to_keycloak.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from math import ceil
23

34
from django.contrib.auth import get_user_model
45
from django.core.management import call_command
@@ -20,11 +21,25 @@ def kc_settings(settings):
2021
settings.KEYCLOAK_REALM_NAME = "pytest-realm"
2122

2223

23-
def test_migrate_users_to_keycloak(mocked_responses):
24+
@pytest.mark.parametrize(
25+
"num_users, num_already_synced, batch_size",
26+
[
27+
(100, 0, 10),
28+
(100, 20, 10),
29+
(100, 99, 10),
30+
],
31+
)
32+
def test_migrate_users_to_keycloak(
33+
mocked_responses,
34+
django_assert_num_queries,
35+
num_users,
36+
num_already_synced,
37+
batch_size,
38+
):
2439
"""Test that the migrate_users_to_keycloak command functions as expected"""
25-
users = UserFactory.create_batch(100)
40+
users = UserFactory.create_batch(num_users)
2641
inactive_users = UserFactory.create_batch(10, is_active=False)
27-
already_synced = users[:20]
42+
already_synced = users[:num_already_synced]
2843

2944
UserExportToKeycloak.objects.bulk_create(
3045
[UserExportToKeycloak(user=user) for user in already_synced]
@@ -68,11 +83,16 @@ def partial_import_callback(request):
6883
content_type="application/json",
6984
)
7085

71-
call_command(
72-
"migrate_users_to_keycloak",
73-
"--client-id=test-client-id",
74-
"--client-secret=test-client-secret",
75-
)
86+
# 3 queries per batch plus 1 for the last empty query to terminate the loop
87+
num_batches = ceil((num_users - num_already_synced) / batch_size)
88+
num_queries = num_batches * 3 + 1
89+
with django_assert_num_queries(num_queries):
90+
call_command(
91+
"migrate_users_to_keycloak",
92+
"--client-id=test-client-id",
93+
"--client-secret=test-client-secret",
94+
f"--batch-size={batch_size}",
95+
)
7696

7797
assert (
7898
list(User.objects.filter(userexporttokeycloak__isnull=False).order_by("id"))

0 commit comments

Comments
 (0)