Skip to content

Commit c42423c

Browse files
meritodawidwolski-identtAsif Saif Uddinn2ygk
authored
Batch tokens deletion in cleartokens command (#969)
* Batch tokens deletion in cleartokens command * CHANGELOG.md and AUTHORS Do not check for merge conflicts in AUTHORS file, because ======= in 2nd line triggers the error. * Issue with AUTHORS file fixed in 1.6.1 Co-authored-by: Dawid Wolski <[email protected]> Co-authored-by: Asif Saif Uddin <[email protected]> Co-authored-by: Alan Crosswell <[email protected]>
1 parent e4c98c7 commit c42423c

File tree

7 files changed

+64
-20
lines changed

7 files changed

+64
-20
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Bas van Oostveen
2424
Dave Burkholder
2525
David Fischer
2626
David Smith
27+
Dawid Wolski
2728
Diego Garcia
2829
Dulmandakh Sukhbaatar
2930
Dylan Giesler

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
-->
1616

1717
## [Unreleased]
18+
### Added
19+
* #651 Batch expired token deletions in `cleartokens` management command
1820

1921
### Added
2022

docs/management_commands.rst

+3
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,8 @@ If ``cleartokens`` runs daily the maximum delay before a refresh token is
1616
removed is ``REFRESH_TOKEN_EXPIRE_SECONDS`` + 1 day. This is normally not a
1717
problem since refresh tokens are long lived.
1818

19+
To prevent the CPU and RAM high peaks during deletion process use ``CLEAR_EXPIRED_TOKENS_BATCH_SIZE`` and
20+
``CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL`` settings to adjust the process speed.
21+
1922
Note: Refresh tokens need to expire before AccessTokens can be removed from the
2023
database. Using ``cleartokens`` without ``REFRESH_TOKEN_EXPIRE_SECONDS`` has limited effect.

docs/settings.rst

+12
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,18 @@ Default: ``["client_secret_post", "client_secret_basic"]``
337337

338338
The authentication methods that are advertised to be supported by this server.
339339

340+
CLEAR_EXPIRED_TOKENS_BATCH_SIZE
341+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
342+
Default: ``10000``
343+
344+
The size of delete batches used by ``cleartokens`` management command.
345+
346+
CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL
347+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
348+
Default: ``0.1``
349+
350+
Time of sleep in seconds used by ``cleartokens`` management command between batch deletions.
351+
340352

341353
Settings imported from Django project
342354
--------------------------

oauth2_provider/models.py

+41-20
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import time
23
import uuid
34
from datetime import timedelta
45
from urllib.parse import parse_qsl, urlparse
@@ -621,12 +622,31 @@ def get_refresh_token_admin_class():
621622

622623

623624
def clear_expired():
625+
def batch_delete(queryset, query):
626+
CLEAR_EXPIRED_TOKENS_BATCH_SIZE = oauth2_settings.CLEAR_EXPIRED_TOKENS_BATCH_SIZE
627+
CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL = oauth2_settings.CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL
628+
current_no = start_no = queryset.count()
629+
630+
while current_no:
631+
flat_queryset = queryset.values_list("id", flat=True)[:CLEAR_EXPIRED_TOKENS_BATCH_SIZE]
632+
batch_length = flat_queryset.count()
633+
queryset.model.objects.filter(id__in=list(flat_queryset)).delete()
634+
logger.debug(f"{batch_length} tokens deleted, {current_no-batch_length} left")
635+
queryset = queryset.model.objects.filter(query)
636+
time.sleep(CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL)
637+
current_no = queryset.count()
638+
639+
stop_no = queryset.model.objects.filter(query).count()
640+
deleted = start_no - stop_no
641+
return deleted
642+
624643
now = timezone.now()
625644
refresh_expire_at = None
626645
access_token_model = get_access_token_model()
627646
refresh_token_model = get_refresh_token_model()
628647
grant_model = get_grant_model()
629648
REFRESH_TOKEN_EXPIRE_SECONDS = oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS
649+
630650
if REFRESH_TOKEN_EXPIRE_SECONDS:
631651
if not isinstance(REFRESH_TOKEN_EXPIRE_SECONDS, timedelta):
632652
try:
@@ -636,31 +656,32 @@ def clear_expired():
636656
raise ImproperlyConfigured(e)
637657
refresh_expire_at = now - REFRESH_TOKEN_EXPIRE_SECONDS
638658

639-
with transaction.atomic():
640-
if refresh_expire_at:
641-
revoked = refresh_token_model.objects.filter(
642-
revoked__lt=refresh_expire_at,
643-
)
644-
expired = refresh_token_model.objects.filter(
645-
access_token__expires__lt=refresh_expire_at,
646-
)
659+
if refresh_expire_at:
660+
revoked_query = models.Q(revoked__lt=refresh_expire_at)
661+
revoked = refresh_token_model.objects.filter(revoked_query)
662+
663+
revoked_deleted_no = batch_delete(revoked, revoked_query)
664+
logger.info("%s Revoked refresh tokens deleted", revoked_deleted_no)
665+
666+
expired_query = models.Q(access_token__expires__lt=refresh_expire_at)
667+
expired = refresh_token_model.objects.filter(expired_query)
647668

648-
logger.info("%s Revoked refresh tokens to be deleted", revoked.count())
649-
logger.info("%s Expired refresh tokens to be deleted", expired.count())
669+
expired_deleted_no = batch_delete(expired, expired_query)
670+
logger.info("%s Expired refresh tokens deleted", expired_deleted_no)
671+
else:
672+
logger.info("refresh_expire_at is %s. No refresh tokens deleted.", refresh_expire_at)
650673

651-
revoked.delete()
652-
expired.delete()
653-
else:
654-
logger.info("refresh_expire_at is %s. No refresh tokens deleted.", refresh_expire_at)
674+
access_token_query = models.Q(refresh_token__isnull=True, expires__lt=now)
675+
access_tokens = access_token_model.objects.filter(access_token_query)
655676

656-
access_tokens = access_token_model.objects.filter(refresh_token__isnull=True, expires__lt=now)
657-
grants = grant_model.objects.filter(expires__lt=now)
677+
access_tokens_delete_no = batch_delete(access_tokens, access_token_query)
678+
logger.info("%s Expired access tokens deleted", access_tokens_delete_no)
658679

659-
logger.info("%s Expired access tokens to be deleted", access_tokens.count())
660-
logger.info("%s Expired grant tokens to be deleted", grants.count())
680+
grants_query = models.Q(expires__lt=now)
681+
grants = grant_model.objects.filter(grants_query)
661682

662-
access_tokens.delete()
663-
grants.delete()
683+
grants_deleted_no = batch_delete(grants, grants_query)
684+
logger.info("%s Expired grant tokens deleted", grants_deleted_no)
664685

665686

666687
def redirect_to_uri_allowed(uri, allowed_uris):

oauth2_provider/settings.py

+2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@
101101
# Whether to re-create OAuthlibCore on every request.
102102
# Should only be required in testing.
103103
"ALWAYS_RELOAD_OAUTHLIB_CORE": False,
104+
"CLEAR_EXPIRED_TOKENS_BATCH_SIZE": 10000,
105+
"CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL": 0.1,
104106
}
105107

106108
# List of settings that cannot be empty

tests/settings.py

+3
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,6 @@
156156
OAUTH2_PROVIDER_APPLICATION_MODEL = "oauth2_provider.Application"
157157
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "oauth2_provider.RefreshToken"
158158
OAUTH2_PROVIDER_ID_TOKEN_MODEL = "oauth2_provider.IDToken"
159+
160+
CLEAR_EXPIRED_TOKENS_BATCH_SIZE = 1
161+
CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL = 0

0 commit comments

Comments
 (0)