4545
4646logger = logging .getLogger (__name__ )
4747PAGE_SIZE = 20_000
48+ CELERY_PAGE_SIZE = 3_000
4849
4950PENDING_STATUSES = [
5051 ubble_schemas .UbbleIdentificationStatus .PROCESSING ,
@@ -75,10 +76,12 @@ def update_ubble_workflow_with_status(
7576 """
7677 if status in PENDING_STATUSES :
7778 fraud_check .status = subscription_models .FraudCheckStatus .PENDING
79+ fraud_check .updatedAt = datetime .datetime .now (datetime .timezone .utc )
7880 return
7981
8082 if status in CANCELED_STATUSES :
8183 fraud_check .status = subscription_models .FraudCheckStatus .CANCELED
84+ fraud_check .updatedAt = datetime .datetime .now (datetime .timezone .utc )
8285 return
8386
8487 if status not in CONCLUSIVE_STATUSES :
@@ -97,10 +100,12 @@ def update_ubble_workflow(fraud_check: subscription_models.BeneficiaryFraudCheck
97100 status = content .status
98101 if status in PENDING_STATUSES :
99102 fraud_check .status = subscription_models .FraudCheckStatus .PENDING
103+ fraud_check .updatedAt = datetime .datetime .now (datetime .timezone .utc )
100104 return
101105
102106 if status in CANCELED_STATUSES :
103107 fraud_check .status = subscription_models .FraudCheckStatus .CANCELED
108+ fraud_check .updatedAt = datetime .datetime .now (datetime .timezone .utc )
104109 return
105110
106111 if status not in CONCLUSIVE_STATUSES :
@@ -447,30 +452,38 @@ def recover_pending_ubble_applications(dry_run: bool = True) -> None:
447452 """
448453 from pcapi .core .subscription .ubble import tasks as ubble_tasks
449454
455+ if FeatureToggle .WIP_ASYNCHRONOUS_CELERY_UBBLE .is_active ():
456+ stale_ubble_fraud_check_ids = _get_stale_fraud_checks_ids (CELERY_PAGE_SIZE )
457+ for fraud_check_id in stale_ubble_fraud_check_ids :
458+ ubble_tasks .update_ubble_workflow_task .delay (
459+ payload = ubble_schemas .UpdateWorkflowPayload (beneficiary_fraud_check_id = fraud_check_id ).model_dump ()
460+ )
461+
462+ logger .warning (
463+ "Found %d stale ubble application older than 12 hours and tried to update them." ,
464+ len (stale_ubble_fraud_check_ids ),
465+ )
466+ return
467+
450468 pending_ubble_application_counter = 0
451- for pending_ubble_application_fraud_checks in _get_pending_fraud_checks_pages ():
452- pending_ubble_application_counter += len (pending_ubble_application_fraud_checks )
453- for fraud_check in pending_ubble_application_fraud_checks :
454- if FeatureToggle .WIP_ASYNCHRONOUS_CELERY_UBBLE .is_active ():
455- ubble_tasks .update_ubble_workflow_task .delay (
456- payload = ubble_schemas .UpdateWorkflowPayload (beneficiary_fraud_check_id = fraud_check .id ).model_dump ()
469+ for pending_ubble_fraud_check_ids in _get_pending_fraud_checks_pages ():
470+ pending_ubble_application_counter += len (pending_ubble_fraud_check_ids )
471+ for fraud_check in pending_ubble_fraud_check_ids :
472+ try :
473+ with atomic ():
474+ update_ubble_workflow (fraud_check )
475+ except Exception as exc :
476+ logger .error (
477+ "Error while updating pending ubble application" ,
478+ extra = {"fraud_check_id" : fraud_check .id , "ubble_id" : fraud_check .thirdPartyId , "exc" : str (exc )},
479+ )
480+ continue
481+ db .session .refresh (fraud_check )
482+ if fraud_check .status == subscription_models .FraudCheckStatus .PENDING :
483+ logger .error (
484+ "Pending ubble application still pending after 12 hours. This is a problem on the Ubble side." ,
485+ extra = {"fraud_check_id" : fraud_check .id , "ubble_id" : fraud_check .thirdPartyId },
457486 )
458- else :
459- try :
460- with atomic ():
461- update_ubble_workflow (fraud_check )
462- except Exception as exc :
463- logger .error (
464- "Error while updating pending ubble application" ,
465- extra = {"fraud_check_id" : fraud_check .id , "ubble_id" : fraud_check .thirdPartyId , "exc" : str (exc )},
466- )
467- continue
468- db .session .refresh (fraud_check )
469- if fraud_check .status == subscription_models .FraudCheckStatus .PENDING :
470- logger .error (
471- "Pending ubble application still pending after 12 hours. This is a problem on the Ubble side." ,
472- extra = {"fraud_check_id" : fraud_check .id , "ubble_id" : fraud_check .thirdPartyId },
473- )
474487
475488 if pending_ubble_application_counter > 0 :
476489 logger .warning (
@@ -481,6 +494,38 @@ def recover_pending_ubble_applications(dry_run: bool = True) -> None:
481494 logger .info ("No pending ubble application found older than 12 hours. This is good." )
482495
483496
497+ def _get_stale_fraud_checks_ids (page_size : int ) -> typing .Sequence [int ]:
498+ """
499+ Returns the `page_size` first stale and pending fraud checks.
500+ This function only returns the first page, and is meant to be called every hour by recovery workers.
501+ """
502+ from pcapi .celery_tasks .tasks import MAX_RETRY_DURATION
503+ from pcapi .core .subscription .ubble import tasks as ubble_tasks
504+
505+ # Celery workers keep all rate limited tasks in memory so we need to limit task stacking lest the workers get OOMKilled
506+ page_size_limit = ubble_tasks .UBBLE_TASK_RATE_LIMIT * (MAX_RETRY_DURATION / 60 ) # ~ 3500 elements
507+ if page_size > page_size_limit :
508+ raise ValueError (f"{ page_size = } is above { page_size_limit = } " )
509+
510+ # Ubble guarantees an application is processed after 3 hours.
511+ # We give ourselves some extra time and we retrieve the applications that are still pending after 12 hours.
512+ twelve_hours_ago = datetime .date .today () - datetime .timedelta (hours = 12 )
513+ stale_fraud_check_ids_stmt = (
514+ sa .select (subscription_models .BeneficiaryFraudCheck .id )
515+ .filter (
516+ subscription_models .BeneficiaryFraudCheck .type == subscription_models .FraudCheckType .UBBLE ,
517+ subscription_models .BeneficiaryFraudCheck .status .in_ (
518+ [subscription_models .FraudCheckStatus .STARTED , subscription_models .FraudCheckStatus .PENDING ]
519+ ),
520+ subscription_models .BeneficiaryFraudCheck .updatedAt < twelve_hours_ago ,
521+ )
522+ .order_by (subscription_models .BeneficiaryFraudCheck .id )
523+ .limit (page_size )
524+ )
525+
526+ return db .session .scalars (stale_fraud_check_ids_stmt ).all ()
527+
528+
484529def _get_pending_fraud_checks_pages () -> typing .Generator [list [subscription_models .BeneficiaryFraudCheck ], None , None ]:
485530 # Ubble guarantees an application is processed after 3 hours.
486531 # We give ourselves some extra time and we retrieve the applications that are still pending after 12 hours.
0 commit comments