Skip to content

Commit 26ac021

Browse files
fix: optimise influxdb calls from handle_api_usage_notifications task (#6458)
1 parent dc4e6e0 commit 26ac021

File tree

3 files changed

+50
-35
lines changed

3 files changed

+50
-35
lines changed

api/organisations/task_helpers.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def _send_api_usage_notification(
9999

100100
def handle_api_usage_notification_for_organisation(organisation: Organisation) -> None:
101101
now = timezone.now()
102+
subscription_cache = organisation.subscription_information_cache
102103

103104
if (
104105
organisation.subscription.is_free_plan
@@ -108,15 +109,7 @@ def handle_api_usage_notification_for_organisation(organisation: Organisation) -
108109
# Default to a rolling month for free accounts or canceled subscriptions.
109110
days = 30
110111
period_starts_at = now - timedelta(days)
111-
elif not organisation.has_subscription_information_cache():
112-
# Since the calling code is a list of many organisations
113-
# log the error and return without raising an exception.
114-
logger.error(
115-
f"Paid organisation {organisation.id} is missing subscription information cache"
116-
)
117-
return
118112
else:
119-
subscription_cache = organisation.subscription_information_cache
120113
billing_starts_at = subscription_cache.current_billing_term_starts_at
121114

122115
if billing_starts_at is None:

api/organisations/tasks.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from .constants import (
3434
ALERT_EMAIL_MESSAGE,
3535
ALERT_EMAIL_SUBJECT,
36+
API_USAGE_ALERT_THRESHOLDS,
3637
API_USAGE_GRACE_PERIOD,
3738
)
3839
from .subscriptions.constants import (
@@ -121,9 +122,20 @@ def finish_subscription_cancellation() -> None:
121122
def handle_api_usage_notifications() -> None:
122123
flagsmith_client = get_client("local", local_eval=True)
123124

124-
for organisation in Organisation.objects.all().select_related(
125-
"subscription", "subscription_information_cache"
126-
):
125+
threshold_percentage = min(API_USAGE_ALERT_THRESHOLDS) / 100
126+
for organisation in Organisation.objects.filter(
127+
# Not having a subscription information cache implies that the organisation has
128+
# no usage so no notification is needed.
129+
subscription_information_cache__isnull=False,
130+
# Skip organisations whose usage in the last 30d is below the threshold of the
131+
# minimum notification level for their allowance. If their usage is below the
132+
# threshold in the last 30d, it must be below it for the current billing period
133+
# (which by definition is <30d).
134+
subscription_information_cache__api_calls_30d__gte=F(
135+
"subscription_information_cache__allowed_30d_api_calls"
136+
)
137+
* threshold_percentage,
138+
).select_related("subscription", "subscription_information_cache"):
127139
feature_enabled = flagsmith_client.get_identity_flags(
128140
organisation.flagsmith_identifier,
129141
traits=organisation.flagsmith_on_flagsmith_api_traits,

api/tests/unit/organisations/test_unit_organisations_tasks.py

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -319,12 +319,22 @@ def test_handle_api_usage_notification_for_organisation_when_cancellation_date_i
319319
mocker: MockerFixture,
320320
) -> None:
321321
# Given
322+
usage = 25
323+
allowance = 1_000_000
324+
322325
organisation.subscription.plan = SCALE_UP
323326
organisation.subscription.subscription_id = "fancy_id"
324327
organisation.subscription.cancellation_date = timezone.now()
325328
organisation.subscription.save()
326329
mock_api_usage = mocker.patch("organisations.task_helpers.get_current_api_usage")
327-
mock_api_usage.return_value = 25
330+
mock_api_usage.return_value = usage
331+
332+
OrganisationSubscriptionInformationCache.objects.create(
333+
organisation=organisation,
334+
allowed_30d_api_calls=allowance,
335+
api_calls_30d=usage,
336+
)
337+
328338
from organisations.task_helpers import logger
329339

330340
logger.addHandler(inspecting_handler)
@@ -353,10 +363,11 @@ def test_handle_api_usage_notification_for_organisation_when_billing_starts_at_i
353363
organisation=organisation,
354364
allowed_30d_api_calls=1_000_000,
355365
current_billing_term_starts_at=billing_term_starts_at,
366+
api_calls_30d=1_900_000,
356367
)
357368

358369
mock_api_usage = mocker.patch("organisations.task_helpers.get_current_api_usage")
359-
mock_api_usage.return_value = 25
370+
mock_api_usage.return_value = 2_000_000
360371

361372
organisation.refresh_from_db()
362373

@@ -379,12 +390,11 @@ def test_handle_api_usage_notifications_when_feature_flag_is_off(
379390
now = timezone.now()
380391
OrganisationSubscriptionInformationCache.objects.create(
381392
organisation=organisation,
382-
allowed_seats=10,
383-
allowed_projects=3,
384393
allowed_30d_api_calls=100,
385394
chargebee_email="test@example.com",
386395
current_billing_term_starts_at=now - timedelta(days=45),
387396
current_billing_term_ends_at=now + timedelta(days=320),
397+
api_calls_30d=110,
388398
)
389399
mock_api_usage = mocker.patch(
390400
"organisations.tasks.get_current_api_usage",
@@ -431,12 +441,10 @@ def test_handle_api_usage_notifications_below_100(
431441
organisation.subscription.save()
432442
OrganisationSubscriptionInformationCache.objects.create(
433443
organisation=organisation,
434-
allowed_seats=10,
435-
allowed_projects=3,
436444
allowed_30d_api_calls=100,
437-
chargebee_email="test@example.com",
438445
current_billing_term_starts_at=now - timedelta(days=45),
439446
current_billing_term_ends_at=now + timedelta(days=320),
447+
api_calls_30d=90,
440448
)
441449
mock_api_usage = mocker.patch(
442450
"organisations.task_helpers.get_current_api_usage",
@@ -465,7 +473,7 @@ def test_handle_api_usage_notifications_below_100(
465473
handle_api_usage_notifications()
466474

467475
# Then
468-
assert len(mock_api_usage.call_args_list) == 2
476+
assert len(mock_api_usage.call_args_list) == 1
469477

470478
# We only care about the call for the main organisation,
471479
# not the call for 'another_organisation'
@@ -551,6 +559,7 @@ def test_handle_api_usage_notifications_below_api_usage_alert_thresholds(
551559
chargebee_email="test@example.com",
552560
current_billing_term_starts_at=now - timedelta(days=45),
553561
current_billing_term_ends_at=now + timedelta(days=320),
562+
api_calls_30d=70,
554563
)
555564
mock_api_usage = mocker.patch(
556565
"organisations.task_helpers.get_current_api_usage",
@@ -571,8 +580,6 @@ def test_handle_api_usage_notifications_below_api_usage_alert_thresholds(
571580
handle_api_usage_notifications()
572581

573582
# Then
574-
mock_api_usage.assert_called_once_with(organisation.id, now - timedelta(days=14))
575-
576583
assert len(mailoutbox) == 0
577584

578585
assert (
@@ -590,23 +597,25 @@ def test_handle_api_usage_notifications_above_100(
590597
mailoutbox: list[EmailMultiAlternatives],
591598
) -> None:
592599
# Given
600+
usage = 105
601+
allowance = 100
602+
593603
now = timezone.now()
594604
organisation.subscription.plan = SCALE_UP
595605
organisation.subscription.subscription_id = "fancy_id"
596606
organisation.subscription.save()
597607
OrganisationSubscriptionInformationCache.objects.create(
598608
organisation=organisation,
599-
allowed_seats=10,
600-
allowed_projects=3,
601-
allowed_30d_api_calls=100,
609+
allowed_30d_api_calls=allowance,
602610
chargebee_email="test@example.com",
603611
current_billing_term_starts_at=now - timedelta(days=45),
604612
current_billing_term_ends_at=now + timedelta(days=320),
613+
api_calls_30d=usage,
605614
)
606615
mock_api_usage = mocker.patch(
607616
"organisations.task_helpers.get_current_api_usage",
608617
)
609-
mock_api_usage.return_value = 105
618+
mock_api_usage.return_value = usage
610619

611620
get_client_mock = mocker.patch("organisations.tasks.get_client")
612621
client_mock = MagicMock()
@@ -693,12 +702,11 @@ def test_handle_api_usage_notifications_with_error(
693702
organisation.subscription.save()
694703
OrganisationSubscriptionInformationCache.objects.create(
695704
organisation=organisation,
696-
allowed_seats=10,
697-
allowed_projects=3,
698705
allowed_30d_api_calls=100,
699706
chargebee_email="test@example.com",
700707
current_billing_term_starts_at=now - timedelta(days=45),
701708
current_billing_term_ends_at=now + timedelta(days=320),
709+
api_calls_30d=100,
702710
)
703711

704712
get_client_mock = mocker.patch("organisations.tasks.get_client")
@@ -738,15 +746,24 @@ def test_handle_api_usage_notifications_for_free_accounts(
738746
mailoutbox: list[EmailMultiAlternatives],
739747
) -> None:
740748
# Given
749+
allowance = MAX_SEATS_IN_FREE_PLAN
750+
usage = MAX_API_CALLS_IN_FREE_PLAN + 5_000
751+
741752
now = timezone.now()
742753
assert organisation.is_paid is False
743754
assert organisation.subscription.is_free_plan is True
744755
assert organisation.subscription.max_api_calls == MAX_API_CALLS_IN_FREE_PLAN
745756

757+
OrganisationSubscriptionInformationCache.objects.create(
758+
organisation=organisation,
759+
allowed_30d_api_calls=allowance,
760+
api_calls_30d=usage,
761+
)
762+
746763
mock_api_usage = mocker.patch(
747764
"organisations.task_helpers.get_current_api_usage",
748765
)
749-
mock_api_usage.return_value = MAX_API_CALLS_IN_FREE_PLAN + 5_000
766+
mock_api_usage.return_value = usage
750767

751768
get_client_mock = mocker.patch("organisations.tasks.get_client")
752769
client_mock = MagicMock()
@@ -831,9 +848,6 @@ def test_handle_api_usage_notifications_missing_info_cache(
831848
organisation.subscription.plan = SCALE_UP
832849
organisation.subscription.save()
833850

834-
from organisations.task_helpers import logger
835-
836-
logger.addHandler(inspecting_handler)
837851
assert organisation.has_subscription_information_cache() is False
838852

839853
mock_api_usage = mocker.patch(
@@ -860,10 +874,6 @@ def test_handle_api_usage_notifications_missing_info_cache(
860874
organisation=organisation,
861875
).exists()
862876

863-
assert inspecting_handler.messages == [ # type: ignore[attr-defined]
864-
f"Paid organisation {organisation.id} is missing subscription information cache"
865-
]
866-
867877

868878
@pytest.mark.parametrize("plan", ("scale-up", "scale-up-v2", "scale-up-v3"))
869879
@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00")

0 commit comments

Comments
 (0)