Skip to content

Commit 2a2fafd

Browse files
committed
Implement retry logic for fetching active Paddle subscriptions and enhance subscription checking in the App component
1 parent c591980 commit 2a2fafd

3 files changed

Lines changed: 75 additions & 44 deletions

File tree

src/backend/base/langflow/api/v1/billing.py

Lines changed: 18 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import asyncio
2-
31
from fastapi import APIRouter, HTTPException
42
from langflow.services.auth.clerk_metadata_constants import PADDLE_SUBSCRIPTION_ID_KEY
53
from lfx.log.logger import logger
@@ -18,9 +16,11 @@
1816
from langflow.services.paddle.provisioning import get_paddle_prices
1917
from langflow.services.paddle.subscriptions import (
2018
ensure_paddle_customer_for_user,
19+
fetch_active_subscription,
2120
get_subscriptions_by_customer_id,
2221
has_active_subscription,
2322
pick_active_subscription,
23+
retry_with_backoff,
2424
)
2525

2626
router = APIRouter(tags=["Billing"], prefix="/billing")
@@ -127,54 +127,34 @@ async def list_paddle_prices():
127127
async def get_subscriptions_by_customer(
128128
current_user: CurrentActiveUser,
129129
) -> dict:
130+
130131
if not get_settings_service().auth_settings.CLERK_AUTH_ENABLED:
131132
raise HTTPException(status_code=400, detail="Clerk auth not enabled")
132133

133134
customer_id = await get_paddle_customer_id_from_clerk_payload()
134135
org_id = get_org_id_from_clerk_payload()
135-
organisation_created_by = await get_organisation_created_by_from_clerk_payload()
136136

137137
if not customer_id:
138138
raise HTTPException(status_code=400, detail="Missing paddle_customer_id")
139-
140-
# 1️⃣ Fetch subscriptions
141-
subscriptions = await get_subscriptions_by_customer_id(
142-
customer_id=customer_id
143-
)
144-
145-
if not subscriptions:
146-
return {
147-
"updated": False,
148-
"reason": "no_subscriptions_found",
149-
}
150-
151-
# 2️⃣ Pick active subscription
139+
140+
subscriptions = await get_subscriptions_by_customer_id(customer_id)
152141
active_sub_id = pick_active_subscription(subscriptions, org_id=org_id)
153-
retry_attempts = 3
154-
retry_delay_seconds = 3
155-
156142
if not active_sub_id:
157-
for attempt in range(retry_attempts):
158-
logger.info(
159-
"No active subscription found on initial lookup, retrying %s/%s after %ss",
160-
attempt + 1,
161-
retry_attempts,
162-
retry_delay_seconds,
143+
try:
144+
active_sub_id, subscriptions = await retry_with_backoff(
145+
lambda: fetch_active_subscription(customer_id, org_id),
146+
retries=3,
147+
base_delay=2,
148+
max_delay=5,
149+
retry_exceptions=(ValueError,), # retry only for this
163150
)
164-
await asyncio.sleep(retry_delay_seconds)
165-
subscriptions = await get_subscriptions_by_customer_id(customer_id=customer_id)
166-
active_sub_id = pick_active_subscription(subscriptions, org_id=org_id)
167-
if active_sub_id:
168-
break
169-
170-
logger.info(f"Active subscription id: {active_sub_id}")
171151

172-
if not active_sub_id:
173-
return {
174-
"updated": False,
175-
"reason": "no_active_subscription",
176-
"subscriptions": subscriptions,
177-
}
152+
except ValueError:
153+
return {
154+
"updated": False,
155+
"reason": "no_active_subscription",
156+
"subscriptions": subscriptions,
157+
}
178158

179159
# 3️⃣ Update Clerk org metadata
180160
await update_clerk_organization(

src/backend/base/langflow/services/paddle/subscriptions.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
import asyncio
66
import re
7-
from typing import Any
7+
from typing import Callable, Any, Awaitable
8+
import random
89

910
from lfx.log.logger import logger
1011
from paddle_billing import Client #noqa: TCH002
@@ -375,4 +376,45 @@ def pick_active_subscription(
375376
status = str(sub.get("status", "")).lower()
376377
if status in ACTIVE_SUBSCRIPTION_STATUSES and str(sub.get("org_id", "")).strip() == org_id:
377378
return sub["id"]
378-
return None
379+
return None
380+
381+
async def fetch_active_subscription(customer_id, org_id):
382+
subs = await get_subscriptions_by_customer_id(customer_id=customer_id)
383+
active_id = pick_active_subscription(subs, org_id=org_id)
384+
385+
if not active_id:
386+
raise ValueError("No active subscription yet")
387+
388+
return active_id, subs
389+
390+
async def retry_with_backoff(
391+
func: Callable[[], Awaitable[Any]],
392+
retries: int = 3,
393+
base_delay: float = 2,
394+
max_delay: float = 10,
395+
jitter: bool = True,
396+
retry_exceptions: tuple = (Exception,),
397+
) -> Any:
398+
last_exception = None
399+
400+
for attempt in range(retries):
401+
try:
402+
return await func()
403+
404+
except retry_exceptions as e:
405+
last_exception = e
406+
407+
delay = min(base_delay * (2 ** attempt), max_delay)
408+
409+
if jitter:
410+
delay += random.uniform(0, 0.5)
411+
412+
logger.info(
413+
"Retry %s/%s failed. Retrying in %.2fs",
414+
attempt + 1,
415+
retries,
416+
delay
417+
)
418+
await asyncio.sleep(delay)
419+
420+
raise last_exception

src/frontend/src/App.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useAuth, useOrganization } from "@clerk/clerk-react";
22
import { initializePaddle } from "@paddle/paddle-js";
3-
import { Suspense, useCallback, useEffect } from "react";
3+
import { Suspense, useCallback, useEffect, useState } from "react";
44
import { RouterProvider } from "react-router-dom";
55
import { IS_CLERK_AUTH } from "@/clerk/auth";
66
import { api } from "@/controllers/API/api";
@@ -24,6 +24,8 @@ export default function App() {
2424
? useAuth()
2525
: { getToken: async () => null };
2626

27+
const [isCheckingSubscription, setIsCheckingSubscription] = useState(false);
28+
2729
const fetchPaddleSubscription = useCallback(async () => {
2830
if (!IS_CLERK_AUTH) {
2931
console.warn(
@@ -32,6 +34,8 @@ export default function App() {
3234
return;
3335
}
3436

37+
setIsCheckingSubscription(true);
38+
3539
try {
3640
const clerkToken = await getToken();
3741

@@ -92,6 +96,8 @@ export default function App() {
9296
}
9397
} catch (error) {
9498
console.error("Failed to fetch Paddle subscription from backend", error);
99+
}finally {
100+
setIsCheckingSubscription(false);
95101
}
96102
}, [getToken, organization?.id]);
97103

@@ -136,8 +142,11 @@ export default function App() {
136142
}, [fetchPaddleSubscription]);
137143

138144
return (
139-
<Suspense fallback={<LoadingPage />}>
140-
<RouterProvider router={router} />
141-
</Suspense>
145+
<>
146+
<Suspense fallback={<LoadingPage />}>
147+
<RouterProvider router={router} />
148+
</Suspense>
149+
{isCheckingSubscription && <LoadingPage overlay />}
150+
</>
142151
);
143152
}

0 commit comments

Comments
 (0)