ethyca-fides 2.67.0rc0__py2.py3-none-any.whl → 2.67.1b0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ethyca-fides might be problematic. Click here for more details.

Files changed (146) hide show
  1. {ethyca_fides-2.67.0rc0.dist-info → ethyca_fides-2.67.1b0.dist-info}/METADATA +1 -1
  2. {ethyca_fides-2.67.0rc0.dist-info → ethyca_fides-2.67.1b0.dist-info}/RECORD +138 -136
  3. fides/_version.py +3 -3
  4. fides/api/common_exceptions.py +4 -0
  5. fides/api/graph/execution.py +16 -0
  6. fides/api/models/privacy_request/privacy_request.py +33 -13
  7. fides/api/schemas/application_config.py +1 -0
  8. fides/api/schemas/connection_configuration/connection_secrets_datahub.py +10 -1
  9. fides/api/service/connectors/base_connector.py +14 -0
  10. fides/api/service/connectors/bigquery_connector.py +5 -0
  11. fides/api/service/connectors/query_configs/bigquery_query_config.py +4 -4
  12. fides/api/service/connectors/query_configs/snowflake_query_config.py +3 -3
  13. fides/api/service/connectors/snowflake_connector.py +55 -2
  14. fides/api/service/connectors/sql_connector.py +107 -9
  15. fides/api/service/privacy_request/request_runner_service.py +3 -2
  16. fides/api/service/privacy_request/request_service.py +173 -32
  17. fides/api/task/execute_request_tasks.py +4 -0
  18. fides/api/task/graph_task.py +48 -2
  19. fides/api/util/cache.py +56 -0
  20. fides/api/util/memory_watchdog.py +286 -0
  21. fides/config/execution_settings.py +8 -0
  22. fides/config/utils.py +1 -0
  23. fides/ui-build/static/admin/404.html +1 -1
  24. fides/ui-build/static/admin/_next/static/chunks/5309-d9a488457898263b.js +1 -0
  25. fides/ui-build/static/admin/_next/static/chunks/{6780-fc7d9ddb1a03e7b3.js → 6780-b42a27e72707936d.js} +1 -1
  26. fides/ui-build/static/admin/_next/static/chunks/7725-539d3a906f627531.js +1 -0
  27. fides/ui-build/static/admin/_next/static/chunks/8735-40caf91800a3610c.js +1 -0
  28. fides/ui-build/static/admin/_next/static/chunks/9046-7085a401297c5520.js +1 -0
  29. fides/ui-build/static/admin/_next/static/chunks/pages/{_app-4b5bff46158a19a3.js → _app-750d6bd16c971bb9.js} +2 -2
  30. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/[monitorId]/[systemId]-a286affa43687eb5.js +1 -0
  31. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/[monitorId]-92b0bd97d8e79340.js +1 -0
  32. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center-ee3c0a103346fc06.js +1 -0
  33. fides/ui-build/static/admin/_next/static/chunks/pages/integrations/[id]-8e346fb36e8034d2.js +1 -0
  34. fides/ui-build/static/admin/_next/static/v1eqRIfzld3di00TTnVM9/_buildManifest.js +1 -0
  35. fides/ui-build/static/admin/add-systems/manual.html +1 -1
  36. fides/ui-build/static/admin/add-systems/multiple.html +1 -1
  37. fides/ui-build/static/admin/add-systems.html +1 -1
  38. fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
  39. fides/ui-build/static/admin/consent/configure.html +1 -1
  40. fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
  41. fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
  42. fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
  43. fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
  44. fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
  45. fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
  46. fides/ui-build/static/admin/consent/properties.html +1 -1
  47. fides/ui-build/static/admin/consent/reporting.html +1 -1
  48. fides/ui-build/static/admin/consent.html +1 -1
  49. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
  50. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
  51. fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
  52. fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
  53. fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
  54. fides/ui-build/static/admin/data-catalog.html +1 -1
  55. fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
  56. fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
  57. fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
  58. fides/ui-build/static/admin/data-discovery/activity.html +1 -1
  59. fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
  60. fides/ui-build/static/admin/data-discovery/detection.html +1 -1
  61. fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
  62. fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
  63. fides/ui-build/static/admin/datamap.html +1 -1
  64. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
  65. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
  66. fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
  67. fides/ui-build/static/admin/dataset/new.html +1 -1
  68. fides/ui-build/static/admin/dataset.html +1 -1
  69. fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
  70. fides/ui-build/static/admin/datastore-connection/new.html +1 -1
  71. fides/ui-build/static/admin/datastore-connection.html +1 -1
  72. fides/ui-build/static/admin/index.html +1 -1
  73. fides/ui-build/static/admin/integrations/[id].html +1 -1
  74. fides/ui-build/static/admin/integrations.html +1 -1
  75. fides/ui-build/static/admin/lib/fides-ext-gpp.js +1 -1
  76. fides/ui-build/static/admin/lib/fides-headless.js +1 -1
  77. fides/ui-build/static/admin/lib/fides-preview.js +1 -1
  78. fides/ui-build/static/admin/lib/fides-tcf.js +4 -4
  79. fides/ui-build/static/admin/lib/fides.js +4 -4
  80. fides/ui-build/static/admin/login/[provider].html +1 -1
  81. fides/ui-build/static/admin/login.html +1 -1
  82. fides/ui-build/static/admin/messaging/[id].html +1 -1
  83. fides/ui-build/static/admin/messaging/add-template.html +1 -1
  84. fides/ui-build/static/admin/messaging.html +1 -1
  85. fides/ui-build/static/admin/poc/ant-components.html +1 -1
  86. fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
  87. fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
  88. fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
  89. fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
  90. fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
  91. fides/ui-build/static/admin/poc/forms.html +1 -1
  92. fides/ui-build/static/admin/poc/table-migration.html +1 -1
  93. fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
  94. fides/ui-build/static/admin/privacy-requests/configure/messaging.html +1 -1
  95. fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
  96. fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
  97. fides/ui-build/static/admin/privacy-requests.html +1 -1
  98. fides/ui-build/static/admin/properties/[id].html +1 -1
  99. fides/ui-build/static/admin/properties/add-property.html +1 -1
  100. fides/ui-build/static/admin/properties.html +1 -1
  101. fides/ui-build/static/admin/reporting/datamap.html +1 -1
  102. fides/ui-build/static/admin/settings/about/alpha.html +1 -1
  103. fides/ui-build/static/admin/settings/about.html +1 -1
  104. fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
  105. fides/ui-build/static/admin/settings/consent.html +1 -1
  106. fides/ui-build/static/admin/settings/custom-fields.html +1 -1
  107. fides/ui-build/static/admin/settings/domain-records.html +1 -1
  108. fides/ui-build/static/admin/settings/domains.html +1 -1
  109. fides/ui-build/static/admin/settings/email-templates.html +1 -1
  110. fides/ui-build/static/admin/settings/locations.html +1 -1
  111. fides/ui-build/static/admin/settings/organization.html +1 -1
  112. fides/ui-build/static/admin/settings/regulations.html +1 -1
  113. fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
  114. fides/ui-build/static/admin/systems/configure/[id].html +1 -1
  115. fides/ui-build/static/admin/systems.html +1 -1
  116. fides/ui-build/static/admin/taxonomy.html +1 -1
  117. fides/ui-build/static/admin/user-management/new.html +1 -1
  118. fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
  119. fides/ui-build/static/admin/user-management.html +1 -1
  120. fides/ui-build/static/admin/_next/static/GKmhMPa_1gMto8JZO8ENy/_buildManifest.js +0 -1
  121. fides/ui-build/static/admin/_next/static/chunks/5309-ce5702b9faeaff55.js +0 -1
  122. fides/ui-build/static/admin/_next/static/chunks/8237-841439bef6682177.js +0 -1
  123. fides/ui-build/static/admin/_next/static/chunks/9046-04a8c092fef1cd83.js +0 -1
  124. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/[monitorId]/[systemId]-7caea7bb58c1f153.js +0 -1
  125. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/[monitorId]-a9a70856f7be1542.js +0 -1
  126. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center-1f0ea5c92ae9a2b4.js +0 -1
  127. fides/ui-build/static/admin/_next/static/chunks/pages/integrations/[id]-f53fe1f2cbebda7c.js +0 -1
  128. {ethyca_fides-2.67.0rc0.dist-info → ethyca_fides-2.67.1b0.dist-info}/WHEEL +0 -0
  129. {ethyca_fides-2.67.0rc0.dist-info → ethyca_fides-2.67.1b0.dist-info}/entry_points.txt +0 -0
  130. {ethyca_fides-2.67.0rc0.dist-info → ethyca_fides-2.67.1b0.dist-info}/licenses/LICENSE +0 -0
  131. {ethyca_fides-2.67.0rc0.dist-info → ethyca_fides-2.67.1b0.dist-info}/top_level.txt +0 -0
  132. /fides/ui-build/static/admin/_next/static/chunks/{3450-ca4ba70da999f264.js → 3450-69f4e16978971bb8.js} +0 -0
  133. /fides/ui-build/static/admin/_next/static/chunks/{4608-43acf39319177bee.js → 4608-f16f281f2d05d963.js} +0 -0
  134. /fides/ui-build/static/admin/_next/static/chunks/{6662-4392ba1e4c254ef7.js → 6662-a9e54ead3dc53644.js} +0 -0
  135. /fides/ui-build/static/admin/_next/static/chunks/{6954-9c4912fbce87c4df.js → 6954-ba98e778a5b45ebf.js} +0 -0
  136. /fides/ui-build/static/admin/_next/static/chunks/pages/{data-catalog-d5b01abcb76792ce.js → data-catalog-7770a8dc34bd0fc0.js} +0 -0
  137. /fides/ui-build/static/admin/_next/static/chunks/pages/datastore-connection/{new-8446418c7ad28f77.js → new-c6614583b14dc9f2.js} +0 -0
  138. /fides/ui-build/static/admin/_next/static/chunks/pages/{datastore-connection-0f29b47402292070.js → datastore-connection-3bd77864da523d41.js} +0 -0
  139. /fides/ui-build/static/admin/_next/static/chunks/pages/{integrations-142abe3e3e3e5bf7.js → integrations-7f15cd8538cdc24d.js} +0 -0
  140. /fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/{[id]-1b6b0d703cf59389.js → [id]-79f1576b1126975c.js} +0 -0
  141. /fides/ui-build/static/admin/_next/static/chunks/pages/{privacy-requests-ccd8d9e06cf2d278.js → privacy-requests-96a08c4431b5462c.js} +0 -0
  142. /fides/ui-build/static/admin/_next/static/chunks/pages/reporting/{datamap-fd1a67892056830a.js → datamap-9d1840f8309b706e.js} +0 -0
  143. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/about/{alpha-8f98a4895e74725e.js → alpha-1066f0c202ef744c.js} +0 -0
  144. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{about-8155a35a62fdb5ae.js → about-37ba24a72a06862e.js} +0 -0
  145. /fides/ui-build/static/admin/_next/static/chunks/pages/user-management/{new-bc4eb541906781e6.js → new-de8cb3739ab99c09.js} +0 -0
  146. /fides/ui-build/static/admin/_next/static/{GKmhMPa_1gMto8JZO8ENy → v1eqRIfzld3di00TTnVM9}/_ssgManifest.js +0 -0
@@ -3,11 +3,12 @@ from __future__ import annotations
3
3
  import json
4
4
  from asyncio import sleep
5
5
  from datetime import datetime, timedelta
6
- from typing import Any, Dict, Optional, Set
6
+ from typing import Any, Dict, List, Optional, Set
7
7
 
8
8
  from httpx import AsyncClient
9
9
  from loguru import logger
10
10
  from sqlalchemy import text
11
+ from sqlalchemy.orm import Session
11
12
  from sqlalchemy.sql.elements import TextClause
12
13
 
13
14
  from fides.api.common_exceptions import PrivacyRequestNotFound
@@ -31,6 +32,9 @@ from fides.api.util.cache import (
31
32
  celery_tasks_in_flight,
32
33
  get_async_task_tracking_cache_key,
33
34
  get_cache,
35
+ get_privacy_request_retry_count,
36
+ increment_privacy_request_retry_count,
37
+ reset_privacy_request_retry_count,
34
38
  )
35
39
  from fides.api.util.lock import redis_lock
36
40
  from fides.common.api.v1.urn_registry import PRIVACY_REQUESTS, V1_URL_PREFIX
@@ -350,10 +354,17 @@ def initiate_interrupted_task_requeue_poll() -> None:
350
354
 
351
355
 
352
356
  def get_cached_task_id(entity_id: str) -> Optional[str]:
353
- """Gets the cached task ID for a privacy request or request task by ID."""
357
+ """Gets the cached task ID for a privacy request or request task by ID.
358
+
359
+ Raises Exception if cache operations fail, allowing callers to handle cache failures appropriately.
360
+ """
354
361
  cache: FidesopsRedis = get_cache()
355
- task_id = cache.get(get_async_task_tracking_cache_key(entity_id))
356
- return task_id
362
+ try:
363
+ task_id = cache.get(get_async_task_tracking_cache_key(entity_id))
364
+ return task_id
365
+ except Exception as exc:
366
+ logger.error(f"Failed to get cached task ID for entity {entity_id}: {exc}")
367
+ raise
357
368
 
358
369
 
359
370
  REQUEUE_INTERRUPTED_TASKS_LOCK = "requeue_interrupted_tasks_lock"
@@ -393,6 +404,115 @@ def _get_task_ids_from_dsr_queue(
393
404
  return queued_tasks_ids
394
405
 
395
406
 
407
+ def _cancel_interrupted_tasks_and_error_privacy_request(
408
+ db: Session, privacy_request: PrivacyRequest, error_message: Optional[str] = None
409
+ ) -> None:
410
+ """
411
+ Cancel all tasks associated with an interrupted privacy request and set the privacy request to error state.
412
+
413
+ This function:
414
+ 1. Logs the error message (either provided or default)
415
+ 2. Revokes the main privacy request task and all associated request tasks
416
+ 3. Sets the privacy request status to error
417
+ 4. Creates an error log entry
418
+
419
+ Args:
420
+ db: Database session
421
+ privacy_request: The privacy request to cancel and error
422
+ error_message: Optional error message to log. If not provided, uses default message.
423
+ """
424
+ if error_message:
425
+ logger.error(error_message)
426
+ else:
427
+ logger.error(
428
+ f"Canceling interrupted tasks and marking privacy request {privacy_request.id} as error"
429
+ )
430
+
431
+ # Cancel all associated Celery tasks
432
+ privacy_request.cancel_celery_tasks()
433
+
434
+ # Set privacy request to error state using the existing method
435
+ try:
436
+ privacy_request.error_processing(db)
437
+ logger.info(
438
+ f"Privacy request {privacy_request.id} marked as error due to task interruption"
439
+ )
440
+ except Exception as exc:
441
+ logger.error(
442
+ f"Failed to mark privacy request {privacy_request.id} as error: {exc}"
443
+ )
444
+
445
+
446
+ def _handle_privacy_request_requeue(
447
+ db: Session, privacy_request: PrivacyRequest
448
+ ) -> None:
449
+ """Handle retry logic for a privacy request - either requeue or cancel based on retry count."""
450
+ try:
451
+ # Check retry count and either requeue or cancel based on limit
452
+ current_retry_count = get_privacy_request_retry_count(privacy_request.id)
453
+ max_retries = CONFIG.execution.privacy_request_requeue_retry_count
454
+
455
+ if current_retry_count < max_retries:
456
+ # Increment retry count and attempt requeue
457
+ new_retry_count = increment_privacy_request_retry_count(privacy_request.id)
458
+ logger.info(
459
+ f"Requeuing privacy request {privacy_request.id} "
460
+ f"(attempt {new_retry_count}/{max_retries})"
461
+ )
462
+
463
+ from fides.service.privacy_request.privacy_request_service import ( # pylint: disable=cyclic-import
464
+ PrivacyRequestError,
465
+ _requeue_privacy_request,
466
+ )
467
+
468
+ try:
469
+ _requeue_privacy_request(db, privacy_request)
470
+ except PrivacyRequestError as exc:
471
+ # If requeue fails, cancel tasks and set to error state
472
+ _cancel_interrupted_tasks_and_error_privacy_request(
473
+ db, privacy_request, exc.message
474
+ )
475
+ else:
476
+ # Exceeded retry limit, cancel tasks and set to error state
477
+ _cancel_interrupted_tasks_and_error_privacy_request(
478
+ db,
479
+ privacy_request,
480
+ f"Privacy request {privacy_request.id} exceeded max retry attempts "
481
+ f"({max_retries}), canceling tasks and setting to error state",
482
+ )
483
+ # Reset retry count since we're giving up
484
+ reset_privacy_request_retry_count(privacy_request.id)
485
+
486
+ except Exception as cache_exc:
487
+ # If cache operations fail (Redis down, network issues, etc.), fail safe by canceling
488
+ _cancel_interrupted_tasks_and_error_privacy_request(
489
+ db,
490
+ privacy_request,
491
+ f"Cache operation failed for privacy request {privacy_request.id}, "
492
+ f"failing safe by canceling tasks: {cache_exc}",
493
+ )
494
+
495
+
496
+ def _get_request_task_ids_in_progress(
497
+ db: Session, privacy_request_id: str
498
+ ) -> List[str]:
499
+ """Get the IDs of request tasks that are currently in progress for a privacy request."""
500
+ request_tasks_in_progress = (
501
+ db.query(RequestTask.id)
502
+ .filter(RequestTask.privacy_request_id == privacy_request_id)
503
+ .filter(
504
+ RequestTask.status.in_(
505
+ [
506
+ ExecutionLogStatus.in_processing,
507
+ ExecutionLogStatus.pending,
508
+ ]
509
+ )
510
+ )
511
+ .all()
512
+ )
513
+ return [task[0] for task in request_tasks_in_progress]
514
+
515
+
396
516
  # pylint: disable=too-many-branches
397
517
  @celery_app.task(base=DatabaseTask, bind=True)
398
518
  def requeue_interrupted_tasks(self: DatabaseTask) -> None:
@@ -442,17 +562,40 @@ def requeue_interrupted_tasks(self: DatabaseTask) -> None:
442
562
  )
443
563
 
444
564
  # Get task IDs from the queue in a memory-efficient way
445
- queued_tasks_ids = _get_task_ids_from_dsr_queue(redis_conn)
565
+ try:
566
+ queued_tasks_ids = _get_task_ids_from_dsr_queue(redis_conn)
567
+ except Exception as queue_exc:
568
+ logger.warning(
569
+ f"Failed to get task IDs from queue, skipping queue state checks: {queue_exc}"
570
+ )
571
+ return
446
572
 
447
573
  # Check each privacy request
448
574
  for privacy_request in in_progress_requests:
449
575
  should_requeue = False
450
576
  logger.debug(f"Checking tasks for privacy request {privacy_request.id}")
451
577
 
452
- task_id = get_cached_task_id(privacy_request.id)
578
+ try:
579
+ task_id = get_cached_task_id(privacy_request.id)
580
+ except Exception as cache_exc:
581
+ # If we can't get the task ID due to cache failure, fail safe by canceling
582
+ _cancel_interrupted_tasks_and_error_privacy_request(
583
+ db,
584
+ privacy_request,
585
+ f"Cache failure when getting task ID for privacy request {privacy_request.id}, "
586
+ f"failing safe by canceling tasks: {cache_exc}",
587
+ )
588
+ continue
453
589
 
454
590
  # If the task ID is not cached, we can't check if it's running
591
+ # This means the request is stuck - cancel it
455
592
  if not task_id:
593
+ _cancel_interrupted_tasks_and_error_privacy_request(
594
+ db,
595
+ privacy_request,
596
+ f"No task ID found for privacy request {privacy_request.id}, "
597
+ f"request is stuck without a running task - canceling",
598
+ )
456
599
  continue
457
600
 
458
601
  # Check if the main privacy request task is active
@@ -470,30 +613,36 @@ def requeue_interrupted_tasks(self: DatabaseTask) -> None:
470
613
  )
471
614
  should_requeue = True
472
615
 
473
- request_tasks_in_progress = (
474
- db.query(RequestTask.id)
475
- .filter(RequestTask.privacy_request_id == privacy_request.id)
476
- .filter(
477
- RequestTask.status.in_(
478
- [
479
- ExecutionLogStatus.in_processing,
480
- ExecutionLogStatus.pending,
481
- ]
482
- )
483
- )
484
- .all()
616
+ request_task_ids_in_progress = _get_request_task_ids_in_progress(
617
+ db, privacy_request.id
485
618
  )
486
- request_task_ids_in_progress = [
487
- task[0] for task in request_tasks_in_progress
488
- ]
489
619
 
490
620
  # Check each individual request task
491
621
  for request_task_id in request_task_ids_in_progress:
492
- subtask_id = get_cached_task_id(request_task_id)
622
+ try:
623
+ subtask_id = get_cached_task_id(request_task_id)
624
+ except Exception as cache_exc:
625
+ # If we can't get the subtask ID due to cache failure, fail safe by canceling
626
+ _cancel_interrupted_tasks_and_error_privacy_request(
627
+ db,
628
+ privacy_request,
629
+ f"Cache failure when getting subtask ID for request task {request_task_id} "
630
+ f"(privacy request {privacy_request.id}), failing safe by canceling tasks: {cache_exc}",
631
+ )
632
+ should_requeue = False
633
+ break
493
634
 
494
635
  # If the task ID is not cached, we can't check if it's running
636
+ # This means the subtask is stuck - cancel the entire privacy request
495
637
  if not subtask_id:
496
- continue
638
+ _cancel_interrupted_tasks_and_error_privacy_request(
639
+ db,
640
+ privacy_request,
641
+ f"No task ID found for request task {request_task_id} "
642
+ f"(privacy request {privacy_request.id}), subtask is stuck - canceling privacy request",
643
+ )
644
+ should_requeue = False
645
+ break
497
646
 
498
647
  if (
499
648
  subtask_id not in queued_tasks_ids
@@ -507,12 +656,4 @@ def requeue_interrupted_tasks(self: DatabaseTask) -> None:
507
656
 
508
657
  # Requeue the privacy request if needed
509
658
  if should_requeue:
510
- from fides.service.privacy_request.privacy_request_service import ( # pylint: disable=cyclic-import
511
- PrivacyRequestError,
512
- _requeue_privacy_request,
513
- )
514
-
515
- try:
516
- _requeue_privacy_request(db, privacy_request)
517
- except PrivacyRequestError as exc:
518
- logger.error(exc.message)
659
+ _handle_privacy_request_requeue(db, privacy_request)
@@ -36,6 +36,7 @@ from fides.api.tasks import DSR_QUEUE_NAME, DatabaseTask, celery_app
36
36
  from fides.api.util.cache import cache_task_tracking_key
37
37
  from fides.api.util.collection_util import Row
38
38
  from fides.api.util.logger_context_utils import LoggerContextKeys, log_context
39
+ from fides.api.util.memory_watchdog import memory_limiter
39
40
 
40
41
  # DSR 3.0 task functions
41
42
 
@@ -255,6 +256,7 @@ def queue_downstream_tasks(
255
256
 
256
257
 
257
258
  @celery_app.task(base=DatabaseTask, bind=True)
259
+ @memory_limiter
258
260
  @log_context(
259
261
  capture_args={
260
262
  "privacy_request_id": LoggerContextKeys.privacy_request_id,
@@ -319,6 +321,7 @@ def run_access_node(
319
321
 
320
322
 
321
323
  @celery_app.task(base=DatabaseTask, bind=True)
324
+ @memory_limiter
322
325
  @log_context(
323
326
  capture_args={
324
327
  "privacy_request_id": LoggerContextKeys.privacy_request_id,
@@ -391,6 +394,7 @@ def run_erasure_node(
391
394
 
392
395
 
393
396
  @celery_app.task(base=DatabaseTask, bind=True)
397
+ @memory_limiter
394
398
  @log_context(
395
399
  capture_args={
396
400
  "privacy_request_id": LoggerContextKeys.privacy_request_id,
@@ -18,6 +18,7 @@ from fides.api.common_exceptions import (
18
18
  NotSupportedForCollection,
19
19
  PrivacyRequestErasureEmailSendRequired,
20
20
  SkippingConsentPropagation,
21
+ TableNotFound,
21
22
  )
22
23
  from fides.api.graph.config import (
23
24
  ROOT_COLLECTION_ADDRESS,
@@ -61,6 +62,7 @@ from fides.api.util.consent_util import (
61
62
  )
62
63
  from fides.api.util.logger import Pii
63
64
  from fides.api.util.logger_context_utils import LoggerContextKeys
65
+ from fides.api.util.memory_watchdog import MemoryLimitExceeded
64
66
  from fides.api.util.saas_util import FIDESOPS_GROUPED_INPUTS
65
67
  from fides.config import CONFIG
66
68
 
@@ -70,6 +72,16 @@ EMPTY_REQUEST = PrivacyRequest()
70
72
  EMPTY_REQUEST_TASK = RequestTask()
71
73
 
72
74
 
75
+ def _is_memory_limit_exceeded(exception: BaseException) -> bool:
76
+ """Check if the exception or any exception in its chain is a MemoryLimitExceeded."""
77
+ current_exception: Optional[BaseException] = exception
78
+ while current_exception:
79
+ if isinstance(current_exception, MemoryLimitExceeded):
80
+ return True
81
+ current_exception = current_exception.__cause__ or current_exception.__context__
82
+ return False
83
+
84
+
73
85
  def retry(
74
86
  action_type: ActionType,
75
87
  default_return: Any,
@@ -126,6 +138,7 @@ def retry(
126
138
  CollectionDisabled,
127
139
  ActionDisabled,
128
140
  NotSupportedForCollection,
141
+ TableNotFound,
129
142
  ) as exc:
130
143
  logger.warning(
131
144
  "{} - Skipping collection {} for privacy_request: {}",
@@ -144,7 +157,31 @@ def retry(
144
157
  self.log_skipped(action_type, exc)
145
158
  self.cache_system_status_for_preferences()
146
159
  return default_return
160
+ except MemoryLimitExceeded as ex:
161
+ # Hard failure – mark task & downstream as errored and abort.
162
+ logger.error(
163
+ "Memory watchdog exceeded ({}%). Aborting {} {} without retry.",
164
+ ex.memory_percent,
165
+ method_name,
166
+ self.execution_node.address,
167
+ )
168
+ # Persist error status and create execution logs before raising
169
+ self.log_end(action_type, ex)
170
+ self.add_error_status_for_consent_reporting()
171
+ raise
147
172
  except BaseException as ex: # pylint: disable=W0703
173
+ # Check if this exception was caused by memory limit exceeded
174
+ if _is_memory_limit_exceeded(ex):
175
+ logger.error(
176
+ "Memory watchdog exceeded (wrapped exception). Aborting {} {} without retry.",
177
+ method_name,
178
+ self.execution_node.address,
179
+ )
180
+ # Persist error status and create execution logs before raising
181
+ self.log_end(action_type, ex)
182
+ self.add_error_status_for_consent_reporting()
183
+ raise
184
+
148
185
  traceback.print_exc()
149
186
  func_delay *= CONFIG.execution.task_retry_backoff
150
187
  logger.warning(
@@ -553,12 +590,21 @@ class GraphTask(ABC): # pylint: disable=too-many-instance-attributes
553
590
  # For access request results, mutate rows in-place to remove non-matching
554
591
  # array elements. We already iterated over `output` above, so reuse the same
555
592
  # loop structure to keep cache locality.
593
+ logger.info(
594
+ "Filtering {} rows in {} for matching array elements.",
595
+ len(output),
596
+ self.execution_node.address,
597
+ )
556
598
  for row in output:
599
+ filter_element_match(row, post_processed_node_input_data)
600
+
601
+ if len(output) > 0:
557
602
  logger.info(
558
- "Filtering row in {} for matching array elements.",
603
+ "Filtering completed for {} rows in {}. Post-processed node size: {}",
604
+ len(output),
559
605
  self.execution_node.address,
606
+ len(post_processed_node_input_data),
560
607
  )
561
- filter_element_match(row, post_processed_node_input_data)
562
608
 
563
609
  if self.request_task.id:
564
610
  # Saves intermediate access results for DSR 3.0 directly on the Request Task
fides/api/util/cache.py CHANGED
@@ -334,6 +334,62 @@ def cache_task_tracking_key(request_id: str, celery_task_id: str) -> None:
334
334
  )
335
335
 
336
336
 
337
+ def get_privacy_request_retry_cache_key(privacy_request_id: str) -> str:
338
+ """Get cache key for tracking privacy request requeue retry attempts."""
339
+ return f"id-{privacy_request_id}-privacy-request-retry-count"
340
+
341
+
342
+ def get_privacy_request_retry_count(privacy_request_id: str) -> int:
343
+ """Get the current retry count for a privacy request requeue attempts.
344
+
345
+ Raises Exception if cache operations fail, allowing callers to handle cache failures appropriately.
346
+ """
347
+ cache: FidesopsRedis = get_cache()
348
+ try:
349
+ retry_count = cache.get(get_privacy_request_retry_cache_key(privacy_request_id))
350
+ return int(retry_count) if retry_count else 0
351
+ except Exception as exc:
352
+ logger.error(
353
+ f"Failed to get retry count for privacy request {privacy_request_id}: {exc}"
354
+ )
355
+ raise
356
+
357
+
358
+ def increment_privacy_request_retry_count(privacy_request_id: str) -> int:
359
+ """Increment and return the retry count for a privacy request requeue attempts.
360
+
361
+ Raises Exception if cache operations fail, allowing callers to handle cache failures appropriately.
362
+ """
363
+ cache: FidesopsRedis = get_cache()
364
+ cache_key = get_privacy_request_retry_cache_key(privacy_request_id)
365
+
366
+ try:
367
+ # Increment the counter, will be 1 if key doesn't exist
368
+ new_count = cache.incr(cache_key)
369
+ # Set expiry to prevent cache buildup (24 hours)
370
+ cache.expire(cache_key, 86400)
371
+ return new_count
372
+ except Exception as exc:
373
+ logger.error(
374
+ f"Failed to increment retry count for privacy request {privacy_request_id}: {exc}"
375
+ )
376
+ raise
377
+
378
+
379
+ def reset_privacy_request_retry_count(privacy_request_id: str) -> None:
380
+ """Reset the retry count for a privacy request requeue attempts.
381
+
382
+ Silently fails if cache operations fail since this is cleanup.
383
+ """
384
+ cache: FidesopsRedis = get_cache()
385
+ try:
386
+ cache.delete(get_privacy_request_retry_cache_key(privacy_request_id))
387
+ except Exception as exc:
388
+ logger.warning(
389
+ f"Failed to reset retry count for privacy request {privacy_request_id}: {exc}"
390
+ )
391
+
392
+
337
393
  def celery_tasks_in_flight(celery_task_ids: List[str]) -> bool:
338
394
  """Returns True if supplied Celery Tasks appear to be in-flight"""
339
395
  if not celery_task_ids: