Skip to content

Commit 5428407

Browse files
authored
fix: update all journals in bulk (#19055)
* fix: update all journals in bulk Issues with the previous code: - all journal objects were loaded into memory, bloating the process for accounts with high volumes, only to be updated, and not used otherwise - once loaded, iterating though each to update is a classic n+1 performance issue, and in some cases hitting a 60 second timeout With this code: - perform a single SQL query to update all relevant records - doesn't load any journal objects into memory - since nothing is in memory, we don't need to synchronize with sqlalchemy session map, making this even more efficient - no longer needs a manual flush Fixes #19033 Signed-off-by: Mike Fiedler <miketheman@gmail.com> * make translations Signed-off-by: Mike Fiedler <miketheman@gmail.com> --------- Signed-off-by: Mike Fiedler <miketheman@gmail.com>
1 parent e0aa7bf commit 5428407

File tree

3 files changed

+50
-61
lines changed

3 files changed

+50
-61
lines changed

warehouse/admin/views/users.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
from paginate_sqlalchemy import SqlalchemyOrmPage as SQLAlchemyORMPage
1717
from pyramid.httpexceptions import HTTPBadRequest, HTTPMovedPermanently, HTTPSeeOther
1818
from pyramid.view import view_config
19-
from sqlalchemy import func, or_, select
20-
from sqlalchemy.orm import joinedload
19+
from sqlalchemy import func, or_, select, update
2120

2221
from warehouse.accounts.interfaces import (
2322
BurnedRecoveryCode,
@@ -342,17 +341,14 @@ def _nuke_user(user, request):
342341

343342
# Update all journals to point to `deleted-user` instead
344343
deleted_user = request.db.query(User).filter(User.username == "deleted-user").one()
345-
346-
journals = (
347-
request.db.query(JournalEntry)
348-
.options(joinedload(JournalEntry.submitted_by))
349-
.filter(JournalEntry.submitted_by == user)
350-
.all()
344+
# Update in bulk to avoid loading all journal entries into memory (n+1)
345+
request.db.execute(
346+
update(JournalEntry)
347+
.where(JournalEntry._submitted_by == user.username)
348+
.values(_submitted_by=deleted_user.username)
349+
.execution_options(synchronize_session=False)
351350
)
352351

353-
for journal in journals:
354-
journal.submitted_by = deleted_user
355-
356352
# Prohibit the username
357353
request.db.add(
358354
ProhibitedUserName(

warehouse/locale/messages.pot

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ msgstr ""
164164
msgid "Successful WebAuthn assertion"
165165
msgstr ""
166166

167-
#: warehouse/accounts/views.py:725 warehouse/manage/views/__init__.py:862
167+
#: warehouse/accounts/views.py:725 warehouse/manage/views/__init__.py:855
168168
msgid "Recovery code accepted. The supplied code cannot be used again."
169169
msgstr ""
170170

@@ -519,143 +519,143 @@ msgid ""
519519
"less."
520520
msgstr ""
521521

522-
#: warehouse/manage/views/__init__.py:316
522+
#: warehouse/manage/views/__init__.py:315
523523
msgid "Account details updated"
524524
msgstr ""
525525

526-
#: warehouse/manage/views/__init__.py:347
526+
#: warehouse/manage/views/__init__.py:346
527527
#, python-brace-format
528528
msgid "Email ${email_address} added - check your email for a verification link"
529529
msgstr ""
530530

531-
#: warehouse/manage/views/__init__.py:809
531+
#: warehouse/manage/views/__init__.py:802
532532
msgid "Recovery codes already generated"
533533
msgstr ""
534534

535-
#: warehouse/manage/views/__init__.py:811
535+
#: warehouse/manage/views/__init__.py:804
536536
msgid "Generating new recovery codes will invalidate your existing codes."
537537
msgstr ""
538538

539-
#: warehouse/manage/views/__init__.py:919
539+
#: warehouse/manage/views/__init__.py:912
540540
msgid "Verify your email to create an API token."
541541
msgstr ""
542542

543-
#: warehouse/manage/views/__init__.py:1021
543+
#: warehouse/manage/views/__init__.py:1014
544544
msgid "API Token does not exist."
545545
msgstr ""
546546

547-
#: warehouse/manage/views/__init__.py:1053
547+
#: warehouse/manage/views/__init__.py:1046
548548
msgid "Invalid credentials. Try again"
549549
msgstr ""
550550

551-
#: warehouse/manage/views/__init__.py:1172
551+
#: warehouse/manage/views/__init__.py:1165
552552
msgid "Invalid alternate repository location details"
553553
msgstr ""
554554

555-
#: warehouse/manage/views/__init__.py:1210
555+
#: warehouse/manage/views/__init__.py:1203
556556
#, python-brace-format
557557
msgid "Added alternate repository '${name}'"
558558
msgstr ""
559559

560-
#: warehouse/manage/views/__init__.py:1243
561-
#: warehouse/manage/views/__init__.py:1504
562-
#: warehouse/manage/views/__init__.py:1589
563-
#: warehouse/manage/views/__init__.py:1690
564-
#: warehouse/manage/views/__init__.py:1790
560+
#: warehouse/manage/views/__init__.py:1236
561+
#: warehouse/manage/views/__init__.py:1497
562+
#: warehouse/manage/views/__init__.py:1582
563+
#: warehouse/manage/views/__init__.py:1683
564+
#: warehouse/manage/views/__init__.py:1783
565565
msgid "Confirm the request"
566566
msgstr ""
567567

568-
#: warehouse/manage/views/__init__.py:1255
568+
#: warehouse/manage/views/__init__.py:1248
569569
msgid "Invalid alternate repository id"
570570
msgstr ""
571571

572-
#: warehouse/manage/views/__init__.py:1266
572+
#: warehouse/manage/views/__init__.py:1259
573573
msgid "Invalid alternate repository for project"
574574
msgstr ""
575575

576-
#: warehouse/manage/views/__init__.py:1275
576+
#: warehouse/manage/views/__init__.py:1268
577577
#, python-brace-format
578578
msgid ""
579579
"Could not delete alternate repository - ${confirm} is not the same as "
580580
"${alt_repo_name}"
581581
msgstr ""
582582

583-
#: warehouse/manage/views/__init__.py:1305
583+
#: warehouse/manage/views/__init__.py:1298
584584
#, python-brace-format
585585
msgid "Deleted alternate repository '${name}'"
586586
msgstr ""
587587

588-
#: warehouse/manage/views/__init__.py:1373
589-
#: warehouse/manage/views/__init__.py:1674
590-
#: warehouse/manage/views/__init__.py:1782
588+
#: warehouse/manage/views/__init__.py:1366
589+
#: warehouse/manage/views/__init__.py:1667
590+
#: warehouse/manage/views/__init__.py:1775
591591
msgid ""
592592
"Project deletion temporarily disabled. See https://pypi.org/help#admin-"
593593
"intervention for details."
594594
msgstr ""
595595

596-
#: warehouse/manage/views/__init__.py:1517
596+
#: warehouse/manage/views/__init__.py:1510
597597
msgid "Could not yank release - "
598598
msgstr ""
599599

600-
#: warehouse/manage/views/__init__.py:1602
600+
#: warehouse/manage/views/__init__.py:1595
601601
msgid "Could not un-yank release - "
602602
msgstr ""
603603

604-
#: warehouse/manage/views/__init__.py:1703
604+
#: warehouse/manage/views/__init__.py:1696
605605
msgid "Could not delete release - "
606606
msgstr ""
607607

608-
#: warehouse/manage/views/__init__.py:1802
608+
#: warehouse/manage/views/__init__.py:1795
609609
msgid "Could not find file"
610610
msgstr ""
611611

612-
#: warehouse/manage/views/__init__.py:1807
612+
#: warehouse/manage/views/__init__.py:1800
613613
msgid "Could not delete file - "
614614
msgstr ""
615615

616-
#: warehouse/manage/views/__init__.py:1957
616+
#: warehouse/manage/views/__init__.py:1950
617617
#, python-brace-format
618618
msgid "Team '${team_name}' already has ${role_name} role for project"
619619
msgstr ""
620620

621-
#: warehouse/manage/views/__init__.py:2064
621+
#: warehouse/manage/views/__init__.py:2057
622622
#, python-brace-format
623623
msgid "User '${username}' already has ${role_name} role for project"
624624
msgstr ""
625625

626-
#: warehouse/manage/views/__init__.py:2131
626+
#: warehouse/manage/views/__init__.py:2124
627627
#, python-brace-format
628628
msgid "${username} is now ${role} of the '${project_name}' project."
629629
msgstr ""
630630

631-
#: warehouse/manage/views/__init__.py:2163
631+
#: warehouse/manage/views/__init__.py:2156
632632
#, python-brace-format
633633
msgid ""
634634
"User '${username}' does not have a verified primary email address and "
635635
"cannot be added as a ${role_name} for project"
636636
msgstr ""
637637

638-
#: warehouse/manage/views/__init__.py:2176
638+
#: warehouse/manage/views/__init__.py:2169
639639
#: warehouse/manage/views/organizations.py:977
640640
#, python-brace-format
641641
msgid "User '${username}' already has an active invite. Please try again later."
642642
msgstr ""
643643

644-
#: warehouse/manage/views/__init__.py:2241
644+
#: warehouse/manage/views/__init__.py:2234
645645
#: warehouse/manage/views/organizations.py:1052
646646
#, python-brace-format
647647
msgid "Invitation sent to '${username}'"
648648
msgstr ""
649649

650-
#: warehouse/manage/views/__init__.py:2273
650+
#: warehouse/manage/views/__init__.py:2266
651651
msgid "Could not find role invitation."
652652
msgstr ""
653653

654-
#: warehouse/manage/views/__init__.py:2284
654+
#: warehouse/manage/views/__init__.py:2277
655655
msgid "Invitation already expired."
656656
msgstr ""
657657

658-
#: warehouse/manage/views/__init__.py:2317
658+
#: warehouse/manage/views/__init__.py:2310
659659
#: warehouse/manage/views/organizations.py:1239
660660
#, python-brace-format
661661
msgid "Invitation revoked from '${username}'."

warehouse/manage/views/__init__.py

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@
1414
HTTPSeeOther,
1515
)
1616
from pyramid.view import view_config, view_defaults
17-
from sqlalchemy import func
17+
from sqlalchemy import func, update
1818
from sqlalchemy.exc import NoResultFound
19-
from sqlalchemy.orm import joinedload
2019
from venusian import lift
2120
from webauthn.helpers import bytes_to_base64url
2221
from webob.multidict import MultiDict
@@ -467,20 +466,14 @@ def delete_account(self):
467466
deleted_user = (
468467
self.request.db.query(User).filter(User.username == "deleted-user").one()
469468
)
470-
471-
journals = (
472-
self.request.db.query(JournalEntry)
473-
.options(joinedload(JournalEntry.submitted_by))
474-
.filter(JournalEntry.submitted_by == self.request.user)
475-
.all()
469+
# Update in bulk to avoid loading all journal entries into memory (n+1)
470+
self.request.db.execute(
471+
update(JournalEntry)
472+
.where(JournalEntry._submitted_by == self.request.user.username)
473+
.values(_submitted_by=deleted_user.username)
474+
.execution_options(synchronize_session=False)
476475
)
477476

478-
for journal in journals:
479-
journal.submitted_by = deleted_user
480-
481-
# Attempt to flush to identify any integrity errors before sending an email
482-
self.request.db.flush() # to identify any integrity errors
483-
484477
# Send a notification email
485478
send_account_deletion_email(self.request, self.request.user)
486479

0 commit comments

Comments
 (0)