mage-ai 0.9.56__py3-none-any.whl → 0.9.57__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 mage-ai might be problematic. Click here for more details.

Files changed (340) hide show
  1. mage_ai/api/policies/GlobalHookPolicy.py +3 -0
  2. mage_ai/api/policies/ProjectPolicy.py +2 -0
  3. mage_ai/api/presenters/ProjectPresenter.py +3 -0
  4. mage_ai/api/resources/GlobalHookResource.py +55 -10
  5. mage_ai/api/resources/ProjectResource.py +15 -2
  6. mage_ai/cache/dbt/cache.py +1 -1
  7. mage_ai/cache/dbt/utils.py +22 -2
  8. mage_ai/data_preparation/models/block/__init__.py +15 -5
  9. mage_ai/data_preparation/models/block/dbt/block_sql.py +12 -3
  10. mage_ai/data_preparation/models/file.py +1 -1
  11. mage_ai/data_preparation/models/global_hooks/models.py +2 -2
  12. mage_ai/data_preparation/models/project/__init__.py +28 -1
  13. mage_ai/orchestration/db/models/schedules.py +64 -102
  14. mage_ai/orchestration/db/models/schedules_project_platform.py +365 -0
  15. mage_ai/orchestration/pipeline_scheduler.py +5 -1700
  16. mage_ai/orchestration/pipeline_scheduler_original.py +1635 -0
  17. mage_ai/orchestration/pipeline_scheduler_project_platform.py +1701 -0
  18. mage_ai/server/constants.py +1 -1
  19. mage_ai/server/frontend_dist/404.html +2 -2
  20. mage_ai/server/frontend_dist/_next/static/A7VXVGKgLQCukXcjdysDz/_buildManifest.js +1 -0
  21. mage_ai/server/frontend_dist/_next/static/chunks/1557-724bfc3eabd095f7.js +1 -0
  22. mage_ai/server/frontend_dist/_next/static/chunks/161-33f26e485ddef393.js +1 -0
  23. mage_ai/server/frontend_dist/_next/static/chunks/3437-ce26fc28e114b44e.js +1 -0
  24. mage_ai/server/frontend_dist/_next/static/chunks/3540-9bb48b08f439d0a0.js +1 -0
  25. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/3745-51be3a2b7cd04895.js → frontend_dist/_next/static/chunks/3745-61b1c63bb56bb670.js} +1 -1
  26. mage_ai/server/frontend_dist/_next/static/chunks/{5729.0f2748e9e6dab951.js → 5189.dca121eccea793be.js} +1 -1
  27. mage_ai/server/frontend_dist/_next/static/chunks/5457-b373ebdf4d05d8e2.js +1 -0
  28. mage_ai/server/frontend_dist/_next/static/chunks/5533-3455832bc3f8429b.js +1 -0
  29. mage_ai/server/frontend_dist/_next/static/chunks/5729-9d8204ab91da631d.js +1 -0
  30. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/5810-e26a0768db1cfdba.js → frontend_dist/_next/static/chunks/5810-addfa3491ae6b01c.js} +1 -1
  31. mage_ai/server/frontend_dist/_next/static/chunks/8264-68db2a8334ad48f1.js +1 -0
  32. mage_ai/server/frontend_dist/_next/static/chunks/8432-0ace6fb7bdbc6864.js +1 -0
  33. mage_ai/server/frontend_dist/_next/static/chunks/8731-82571147875a2d58.js +1 -0
  34. mage_ai/server/frontend_dist/_next/static/chunks/{9264-a5e8a0274efa2b14.js → 9264-5df6e4c7b1e85c02.js} +1 -1
  35. mage_ai/server/frontend_dist/_next/static/chunks/pages/{_app-cc7f5a0c2456bc03.js → _app-4c0239ca6203e827.js} +1 -1
  36. mage_ai/server/frontend_dist/_next/static/chunks/pages/compute-822e66aa2e90cf4c.js +1 -0
  37. mage_ai/server/frontend_dist/_next/static/chunks/pages/files-373217c5de51aeef.js +1 -0
  38. mage_ai/server/frontend_dist/_next/static/chunks/pages/global-data-products/{[...slug]-1e4838e534c8f31e.js → [...slug]-0325e76a2f3e08c1.js} +1 -1
  39. mage_ai/server/frontend_dist/_next/static/chunks/pages/{global-data-products-bb38f73540efeac4.js → global-data-products-927ebbdc29529765.js} +1 -1
  40. mage_ai/server/frontend_dist/_next/static/chunks/pages/global-hooks/[...slug]-a172f5a447bd8925.js +1 -0
  41. mage_ai/server/frontend_dist/_next/static/chunks/pages/global-hooks-97bec2ac883e0c26.js +1 -0
  42. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/files-02d001d99eeaae3f.js +1 -0
  43. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/settings-2e577bfd4f0db2b7.js +1 -0
  44. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/users/[user]-1827574a4ba95a72.js +1 -0
  45. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/users/new-a913c361bcc0d1a9.js +1 -0
  46. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/users-4e6fdcbbfc931d67.js +1 -0
  47. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage-1c327edcf05df9c9.js +1 -0
  48. mage_ai/server/frontend_dist/_next/static/chunks/pages/{oauth-eba7027969034415.js → oauth-bd8494f8875c5c97.js} +1 -1
  49. mage_ai/server/frontend_dist/_next/static/chunks/pages/overview-5a98e6a531410afb.js +1 -0
  50. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipeline-runs-1442183d13edec2e.js +1 -0
  51. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills/[...slug]-b526282c8ac49986.js → frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills/[...slug]-38b2241cdd10320c.js} +1 -1
  52. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills-515eb343210aa1fb.js → frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills-1c646dbef65d6a69.js} +1 -1
  53. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/dashboard-fe4d1321da8291cb.js → frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/dashboard-35cb916a18ac4e1f.js} +1 -1
  54. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-cd1918632dfef29d.js +1 -0
  55. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/{logs-c011d465f61138ee.js → logs-67b0572c84db0940.js} +1 -1
  56. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/monitors/{block-runs-46c0dabd155581a0.js → block-runs-40201b626ea3a664.js} +1 -1
  57. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/monitors/{block-runtime-f2a052cb8a20fe47.js → block-runtime-d1f23308effebe03.js} +1 -1
  58. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/monitors-579899cbaedcdefc.js → frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/monitors-9a116d88f00916ff.js} +1 -1
  59. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/runs/[run]-7a2265b7e44bed0b.js → frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/runs/[run]-2d5abcd019d4bea1.js} +1 -1
  60. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/runs-9b53da919db959bd.js → frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/runs-5363a7ae9afe8983.js} +1 -1
  61. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/{settings-1839276725bfd757.js → settings-931a1b3112866a72.js} +1 -1
  62. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/syncs-68d779bc62185470.js → frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/syncs-5ec5367cb877db38.js} +1 -1
  63. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/triggers/[...slug]-01f4528602f4ba2a.js → frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/triggers/[...slug]-8366661f8e2b2959.js} +1 -1
  64. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/{triggers-358523847fdbf547.js → triggers-378f8dada7d0c1dd.js} +1 -1
  65. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines-627be24ef4963cfb.js +1 -0
  66. mage_ai/server/frontend_dist/_next/static/chunks/pages/platform/global-hooks/[...slug]-814bbd11e10c26dc.js +1 -0
  67. mage_ai/server/frontend_dist/_next/static/chunks/pages/platform/global-hooks-021ec25e05862f8f.js +1 -0
  68. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/settings/account/profile-00393e67ab1e6788.js → frontend_dist/_next/static/chunks/pages/settings/account/profile-2b0aa123043519b8.js} +1 -1
  69. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/platform/preferences-05186e17c94347c1.js +1 -0
  70. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/platform/settings-efe8bf1bf3177a7e.js +1 -0
  71. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/permissions/{[...slug]-eaeba99f9547a20a.js → [...slug]-aa5d871de1f3f7b0.js} +1 -1
  72. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/{permissions-c3140516cc28e467.js → permissions-ce45aad47049d993.js} +1 -1
  73. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/preferences-1bc694b056ff0bcb.js +1 -0
  74. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/roles/{[...slug]-f6ff0e219a4b9ffd.js → [...slug]-88d29d1774db67e4.js} +1 -1
  75. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/{roles-22ff9d862736b2ec.js → roles-d8ca763e405cd9d1.js} +1 -1
  76. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/{sync-data-614925e838d1974c.js → sync-data-42bd909eb8951040.js} +1 -1
  77. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/users/{[...slug]-5b88dfb1c6d0d16c.js → [...slug]-250dfdf22bfe67e9.js} +1 -1
  78. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/{users-eb904db7ac7ea57c.js → users-97c4ce119f7238b5.js} +1 -1
  79. mage_ai/server/frontend_dist/_next/static/chunks/pages/{sign-in-c734db1d5834dda2.js → sign-in-1af3ba18ff646dd4.js} +1 -1
  80. mage_ai/server/frontend_dist/_next/static/chunks/pages/templates/[...slug]-ff9d49355393daea.js +1 -0
  81. mage_ai/server/frontend_dist/_next/static/chunks/pages/{templates-5ebfe79c24d8c4b4.js → templates-299a2c8f2dd89cf3.js} +1 -1
  82. mage_ai/server/frontend_dist/_next/static/chunks/pages/terminal-fb3f398009a02879.js +1 -0
  83. mage_ai/server/frontend_dist/_next/static/chunks/pages/test-59a08e06f4ef6c3a.js +1 -0
  84. mage_ai/server/frontend_dist/_next/static/chunks/pages/triggers-551b85802515e313.js +1 -0
  85. mage_ai/server/frontend_dist/_next/static/chunks/pages/version-control-1362aeda4a31dd41.js +1 -0
  86. mage_ai/server/frontend_dist/_next/static/chunks/{webpack-fea697dd168c6d0c.js → webpack-17c3a8f588f14cfd.js} +1 -1
  87. mage_ai/server/frontend_dist/block-layout.html +2 -2
  88. mage_ai/server/frontend_dist/compute.html +2 -2
  89. mage_ai/server/frontend_dist/files.html +2 -2
  90. mage_ai/server/frontend_dist/global-data-products/[...slug].html +2 -2
  91. mage_ai/server/frontend_dist/global-data-products.html +2 -2
  92. mage_ai/server/frontend_dist/global-hooks/[...slug].html +2 -2
  93. mage_ai/server/frontend_dist/global-hooks.html +2 -2
  94. mage_ai/server/frontend_dist/index.html +2 -2
  95. mage_ai/server/frontend_dist/manage/files.html +2 -2
  96. mage_ai/server/frontend_dist/manage/settings.html +2 -2
  97. mage_ai/server/frontend_dist/manage/users/[user].html +2 -2
  98. mage_ai/server/frontend_dist/manage/users/new.html +2 -2
  99. mage_ai/server/frontend_dist/manage/users.html +2 -2
  100. mage_ai/server/frontend_dist/manage.html +2 -2
  101. mage_ai/server/frontend_dist/oauth.html +3 -3
  102. mage_ai/server/frontend_dist/overview.html +2 -2
  103. mage_ai/server/frontend_dist/pipeline-runs.html +2 -2
  104. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  105. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills.html +2 -2
  106. mage_ai/server/frontend_dist/pipelines/[pipeline]/dashboard.html +2 -2
  107. mage_ai/server/frontend_dist/pipelines/[pipeline]/edit.html +2 -2
  108. mage_ai/server/frontend_dist/pipelines/[pipeline]/logs.html +2 -2
  109. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  110. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  111. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors.html +2 -2
  112. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs/[run].html +2 -2
  113. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs.html +2 -2
  114. mage_ai/server/frontend_dist/pipelines/[pipeline]/settings.html +2 -2
  115. mage_ai/server/frontend_dist/pipelines/[pipeline]/syncs.html +2 -2
  116. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  117. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers.html +2 -2
  118. mage_ai/server/frontend_dist/pipelines/[pipeline].html +2 -2
  119. mage_ai/server/frontend_dist/pipelines.html +2 -2
  120. mage_ai/server/frontend_dist/platform/global-hooks/[...slug].html +24 -0
  121. mage_ai/server/frontend_dist/platform/global-hooks.html +24 -0
  122. mage_ai/server/frontend_dist/settings/account/profile.html +2 -2
  123. mage_ai/server/frontend_dist/settings/platform/preferences.html +24 -0
  124. mage_ai/server/frontend_dist/settings/platform/settings.html +24 -0
  125. mage_ai/server/frontend_dist/settings/workspace/permissions/[...slug].html +2 -2
  126. mage_ai/server/frontend_dist/settings/workspace/permissions.html +2 -2
  127. mage_ai/server/frontend_dist/settings/workspace/preferences.html +2 -2
  128. mage_ai/server/frontend_dist/settings/workspace/roles/[...slug].html +2 -2
  129. mage_ai/server/frontend_dist/settings/workspace/roles.html +2 -2
  130. mage_ai/server/frontend_dist/settings/workspace/sync-data.html +2 -2
  131. mage_ai/server/frontend_dist/settings/workspace/users/[...slug].html +2 -2
  132. mage_ai/server/frontend_dist/settings/workspace/users.html +2 -2
  133. mage_ai/server/frontend_dist/settings.html +2 -2
  134. mage_ai/server/frontend_dist/sign-in.html +5 -5
  135. mage_ai/server/frontend_dist/templates/[...slug].html +2 -2
  136. mage_ai/server/frontend_dist/templates.html +2 -2
  137. mage_ai/server/frontend_dist/terminal.html +2 -2
  138. mage_ai/server/frontend_dist/test.html +2 -2
  139. mage_ai/server/frontend_dist/triggers.html +2 -2
  140. mage_ai/server/frontend_dist/version-control.html +2 -2
  141. mage_ai/server/frontend_dist_base_path_template/404.html +4 -4
  142. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/1557-724bfc3eabd095f7.js +1 -0
  143. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/161-33f26e485ddef393.js +1 -0
  144. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/3437-ce26fc28e114b44e.js +1 -0
  145. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/3540-9bb48b08f439d0a0.js +1 -0
  146. mage_ai/server/{frontend_dist/_next/static/chunks/3745-51be3a2b7cd04895.js → frontend_dist_base_path_template/_next/static/chunks/3745-61b1c63bb56bb670.js} +1 -1
  147. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{5729.0f2748e9e6dab951.js → 5189.dca121eccea793be.js} +1 -1
  148. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/5457-b373ebdf4d05d8e2.js +1 -0
  149. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/5533-3455832bc3f8429b.js +1 -0
  150. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/5729-9d8204ab91da631d.js +1 -0
  151. mage_ai/server/{frontend_dist/_next/static/chunks/5810-e26a0768db1cfdba.js → frontend_dist_base_path_template/_next/static/chunks/5810-addfa3491ae6b01c.js} +1 -1
  152. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/8264-68db2a8334ad48f1.js +1 -0
  153. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/8432-0ace6fb7bdbc6864.js +1 -0
  154. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/8731-82571147875a2d58.js +1 -0
  155. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{9264-a5e8a0274efa2b14.js → 9264-5df6e4c7b1e85c02.js} +1 -1
  156. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/{_app-cc7f5a0c2456bc03.js → _app-4c0239ca6203e827.js} +1 -1
  157. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/compute-822e66aa2e90cf4c.js +1 -0
  158. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/files-373217c5de51aeef.js +1 -0
  159. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/global-data-products/{[...slug]-1e4838e534c8f31e.js → [...slug]-0325e76a2f3e08c1.js} +1 -1
  160. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/{global-data-products-bb38f73540efeac4.js → global-data-products-927ebbdc29529765.js} +1 -1
  161. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/global-hooks/[...slug]-a172f5a447bd8925.js +1 -0
  162. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/global-hooks-97bec2ac883e0c26.js +1 -0
  163. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/files-02d001d99eeaae3f.js +1 -0
  164. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/settings-2e577bfd4f0db2b7.js +1 -0
  165. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/users/[user]-1827574a4ba95a72.js +1 -0
  166. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/users/new-a913c361bcc0d1a9.js +1 -0
  167. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/users-4e6fdcbbfc931d67.js +1 -0
  168. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage-1c327edcf05df9c9.js +1 -0
  169. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/{oauth-eba7027969034415.js → oauth-bd8494f8875c5c97.js} +1 -1
  170. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/overview-5a98e6a531410afb.js +1 -0
  171. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipeline-runs-1442183d13edec2e.js +1 -0
  172. mage_ai/server/{frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills/[...slug]-b526282c8ac49986.js → frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills/[...slug]-38b2241cdd10320c.js} +1 -1
  173. mage_ai/server/{frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills-515eb343210aa1fb.js → frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills-1c646dbef65d6a69.js} +1 -1
  174. mage_ai/server/{frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/dashboard-fe4d1321da8291cb.js → frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/dashboard-35cb916a18ac4e1f.js} +1 -1
  175. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/edit-cd1918632dfef29d.js +1 -0
  176. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/{logs-c011d465f61138ee.js → logs-67b0572c84db0940.js} +1 -1
  177. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/monitors/{block-runs-46c0dabd155581a0.js → block-runs-40201b626ea3a664.js} +1 -1
  178. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/monitors/{block-runtime-f2a052cb8a20fe47.js → block-runtime-d1f23308effebe03.js} +1 -1
  179. mage_ai/server/{frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/monitors-579899cbaedcdefc.js → frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/monitors-9a116d88f00916ff.js} +1 -1
  180. mage_ai/server/{frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/runs/[run]-7a2265b7e44bed0b.js → frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/runs/[run]-2d5abcd019d4bea1.js} +1 -1
  181. mage_ai/server/{frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/runs-9b53da919db959bd.js → frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/runs-5363a7ae9afe8983.js} +1 -1
  182. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/{settings-1839276725bfd757.js → settings-931a1b3112866a72.js} +1 -1
  183. mage_ai/server/{frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/syncs-68d779bc62185470.js → frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/syncs-5ec5367cb877db38.js} +1 -1
  184. mage_ai/server/{frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/triggers/[...slug]-01f4528602f4ba2a.js → frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/triggers/[...slug]-8366661f8e2b2959.js} +1 -1
  185. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/{triggers-358523847fdbf547.js → triggers-378f8dada7d0c1dd.js} +1 -1
  186. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines-627be24ef4963cfb.js +1 -0
  187. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/platform/global-hooks/[...slug]-814bbd11e10c26dc.js +1 -0
  188. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/platform/global-hooks-021ec25e05862f8f.js +1 -0
  189. mage_ai/server/{frontend_dist/_next/static/chunks/pages/settings/account/profile-00393e67ab1e6788.js → frontend_dist_base_path_template/_next/static/chunks/pages/settings/account/profile-2b0aa123043519b8.js} +1 -1
  190. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/platform/preferences-05186e17c94347c1.js +1 -0
  191. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/platform/settings-efe8bf1bf3177a7e.js +1 -0
  192. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/permissions/{[...slug]-eaeba99f9547a20a.js → [...slug]-aa5d871de1f3f7b0.js} +1 -1
  193. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/{permissions-c3140516cc28e467.js → permissions-ce45aad47049d993.js} +1 -1
  194. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/preferences-1bc694b056ff0bcb.js +1 -0
  195. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/roles/{[...slug]-f6ff0e219a4b9ffd.js → [...slug]-88d29d1774db67e4.js} +1 -1
  196. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/{roles-22ff9d862736b2ec.js → roles-d8ca763e405cd9d1.js} +1 -1
  197. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/{sync-data-614925e838d1974c.js → sync-data-42bd909eb8951040.js} +1 -1
  198. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/users/{[...slug]-5b88dfb1c6d0d16c.js → [...slug]-250dfdf22bfe67e9.js} +1 -1
  199. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/{users-eb904db7ac7ea57c.js → users-97c4ce119f7238b5.js} +1 -1
  200. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/{sign-in-c734db1d5834dda2.js → sign-in-1af3ba18ff646dd4.js} +1 -1
  201. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/templates/[...slug]-ff9d49355393daea.js +1 -0
  202. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/{templates-5ebfe79c24d8c4b4.js → templates-299a2c8f2dd89cf3.js} +1 -1
  203. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/terminal-fb3f398009a02879.js +1 -0
  204. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/test-59a08e06f4ef6c3a.js +1 -0
  205. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/triggers-551b85802515e313.js +1 -0
  206. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/version-control-1362aeda4a31dd41.js +1 -0
  207. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{webpack-d30cb09c85b4c4f0.js → webpack-b55fe1e575d8ac9d.js} +1 -1
  208. mage_ai/server/frontend_dist_base_path_template/_next/static/wSRrM15xnvA8lOWcaque7/_buildManifest.js +1 -0
  209. mage_ai/server/frontend_dist_base_path_template/block-layout.html +2 -2
  210. mage_ai/server/frontend_dist_base_path_template/compute.html +5 -5
  211. mage_ai/server/frontend_dist_base_path_template/files.html +5 -5
  212. mage_ai/server/frontend_dist_base_path_template/global-data-products/[...slug].html +5 -5
  213. mage_ai/server/frontend_dist_base_path_template/global-data-products.html +5 -5
  214. mage_ai/server/frontend_dist_base_path_template/global-hooks/[...slug].html +5 -5
  215. mage_ai/server/frontend_dist_base_path_template/global-hooks.html +5 -5
  216. mage_ai/server/frontend_dist_base_path_template/index.html +2 -2
  217. mage_ai/server/frontend_dist_base_path_template/manage/files.html +5 -5
  218. mage_ai/server/frontend_dist_base_path_template/manage/settings.html +5 -5
  219. mage_ai/server/frontend_dist_base_path_template/manage/users/[user].html +5 -5
  220. mage_ai/server/frontend_dist_base_path_template/manage/users/new.html +5 -5
  221. mage_ai/server/frontend_dist_base_path_template/manage/users.html +5 -5
  222. mage_ai/server/frontend_dist_base_path_template/manage.html +5 -5
  223. mage_ai/server/frontend_dist_base_path_template/oauth.html +4 -4
  224. mage_ai/server/frontend_dist_base_path_template/overview.html +5 -5
  225. mage_ai/server/frontend_dist_base_path_template/pipeline-runs.html +5 -5
  226. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills/[...slug].html +5 -5
  227. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills.html +5 -5
  228. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/dashboard.html +5 -5
  229. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/edit.html +2 -2
  230. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/logs.html +5 -5
  231. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runs.html +5 -5
  232. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runtime.html +5 -5
  233. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors.html +5 -5
  234. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs/[run].html +5 -5
  235. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs.html +5 -5
  236. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/settings.html +5 -5
  237. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/syncs.html +5 -5
  238. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers/[...slug].html +5 -5
  239. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers.html +5 -5
  240. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline].html +2 -2
  241. mage_ai/server/frontend_dist_base_path_template/pipelines.html +5 -5
  242. mage_ai/server/frontend_dist_base_path_template/platform/global-hooks/[...slug].html +24 -0
  243. mage_ai/server/frontend_dist_base_path_template/platform/global-hooks.html +24 -0
  244. mage_ai/server/frontend_dist_base_path_template/settings/account/profile.html +5 -5
  245. mage_ai/server/frontend_dist_base_path_template/settings/platform/preferences.html +24 -0
  246. mage_ai/server/frontend_dist_base_path_template/settings/platform/settings.html +24 -0
  247. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions/[...slug].html +5 -5
  248. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions.html +5 -5
  249. mage_ai/server/frontend_dist_base_path_template/settings/workspace/preferences.html +5 -5
  250. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles/[...slug].html +5 -5
  251. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles.html +5 -5
  252. mage_ai/server/frontend_dist_base_path_template/settings/workspace/sync-data.html +5 -5
  253. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users/[...slug].html +5 -5
  254. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users.html +5 -5
  255. mage_ai/server/frontend_dist_base_path_template/settings.html +2 -2
  256. mage_ai/server/frontend_dist_base_path_template/sign-in.html +14 -14
  257. mage_ai/server/frontend_dist_base_path_template/templates/[...slug].html +5 -5
  258. mage_ai/server/frontend_dist_base_path_template/templates.html +5 -5
  259. mage_ai/server/frontend_dist_base_path_template/terminal.html +5 -5
  260. mage_ai/server/frontend_dist_base_path_template/test.html +2 -2
  261. mage_ai/server/frontend_dist_base_path_template/triggers.html +5 -5
  262. mage_ai/server/frontend_dist_base_path_template/version-control.html +5 -5
  263. mage_ai/settings/models/configuration_option.py +15 -1
  264. mage_ai/settings/platform/__init__.py +29 -7
  265. mage_ai/streaming/sinks/rabbitmq.py +3 -1
  266. mage_ai/streaming/sources/rabbitmq.py +5 -2
  267. mage_ai/tests/api/endpoints/test_pipeline_runs.py +4 -0
  268. mage_ai/tests/data_preparation/models/test_project.py +27 -7
  269. mage_ai/tests/orchestration/db/models/test_schedules.py +25 -7
  270. mage_ai/tests/orchestration/test_pipeline_scheduler.py +6 -261
  271. mage_ai/tests/orchestration/test_pipeline_scheduler_project_platform.py +286 -0
  272. mage_ai/tests/shared/mixins.py +1 -0
  273. {mage_ai-0.9.56.dist-info → mage_ai-0.9.57.dist-info}/METADATA +1 -1
  274. {mage_ai-0.9.56.dist-info → mage_ai-0.9.57.dist-info}/RECORD +280 -258
  275. mage_ai/server/frontend_dist/_next/static/4hG0vHBR7gnry-ZWEsEF3/_buildManifest.js +0 -1
  276. mage_ai/server/frontend_dist/_next/static/chunks/1125-65883c05178efca1.js +0 -1
  277. mage_ai/server/frontend_dist/_next/static/chunks/1952-c4ba37bc172d7051.js +0 -1
  278. mage_ai/server/frontend_dist/_next/static/chunks/3437-91f6098316edaf17.js +0 -1
  279. mage_ai/server/frontend_dist/_next/static/chunks/5457-6e700aadac17ceec.js +0 -1
  280. mage_ai/server/frontend_dist/_next/static/chunks/7936-e25b6058d3f9b215.js +0 -1
  281. mage_ai/server/frontend_dist/_next/static/chunks/8264-dad1f090c4278137.js +0 -1
  282. mage_ai/server/frontend_dist/_next/static/chunks/8432-ec2b97cbf32ec5a7.js +0 -1
  283. mage_ai/server/frontend_dist/_next/static/chunks/8731-9e0ad513b0dfce83.js +0 -1
  284. mage_ai/server/frontend_dist/_next/static/chunks/9626-090ff01fd210431c.js +0 -1
  285. mage_ai/server/frontend_dist/_next/static/chunks/pages/compute-5ead3afa88d14721.js +0 -1
  286. mage_ai/server/frontend_dist/_next/static/chunks/pages/files-13a4f6d00e8a1c63.js +0 -1
  287. mage_ai/server/frontend_dist/_next/static/chunks/pages/global-hooks/[...slug]-a7d74042d52c3c38.js +0 -1
  288. mage_ai/server/frontend_dist/_next/static/chunks/pages/global-hooks-cf7f1ba0b44ec0fb.js +0 -1
  289. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/files-259dea0c7cb30d2a.js +0 -1
  290. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/settings-1fe2dfaa456529a6.js +0 -1
  291. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/users/[user]-1e8731ba2559fef4.js +0 -1
  292. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/users/new-a6307396dfa82c88.js +0 -1
  293. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/users-fc5aafe5085739a9.js +0 -1
  294. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage-8fad54817f356e9f.js +0 -1
  295. mage_ai/server/frontend_dist/_next/static/chunks/pages/overview-321f47b2dba4c780.js +0 -1
  296. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipeline-runs-bf4d162b5460acc6.js +0 -1
  297. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-ed8c521227dd326e.js +0 -1
  298. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines-33d9fb90871be84c.js +0 -1
  299. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/preferences-f4ff09cfff8bad46.js +0 -1
  300. mage_ai/server/frontend_dist/_next/static/chunks/pages/templates/[...slug]-04b0a55e8ad6f814.js +0 -1
  301. mage_ai/server/frontend_dist/_next/static/chunks/pages/terminal-88025dd0ed3051a6.js +0 -1
  302. mage_ai/server/frontend_dist/_next/static/chunks/pages/test-5b316b190ff4b226.js +0 -1
  303. mage_ai/server/frontend_dist/_next/static/chunks/pages/triggers-e5e49ac3b9282aaa.js +0 -1
  304. mage_ai/server/frontend_dist/_next/static/chunks/pages/version-control-b52d3a07a13452ff.js +0 -1
  305. mage_ai/server/frontend_dist_base_path_template/_next/static/MKDICBWwxQowqsGfMukac/_buildManifest.js +0 -1
  306. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/1125-65883c05178efca1.js +0 -1
  307. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/1952-c4ba37bc172d7051.js +0 -1
  308. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/3437-91f6098316edaf17.js +0 -1
  309. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/5457-6e700aadac17ceec.js +0 -1
  310. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/7936-e25b6058d3f9b215.js +0 -1
  311. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/8264-dad1f090c4278137.js +0 -1
  312. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/8432-ec2b97cbf32ec5a7.js +0 -1
  313. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/8731-9e0ad513b0dfce83.js +0 -1
  314. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/9626-090ff01fd210431c.js +0 -1
  315. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/compute-5ead3afa88d14721.js +0 -1
  316. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/files-13a4f6d00e8a1c63.js +0 -1
  317. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/global-hooks/[...slug]-a7d74042d52c3c38.js +0 -1
  318. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/global-hooks-cf7f1ba0b44ec0fb.js +0 -1
  319. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/files-259dea0c7cb30d2a.js +0 -1
  320. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/settings-1fe2dfaa456529a6.js +0 -1
  321. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/users/[user]-1e8731ba2559fef4.js +0 -1
  322. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/users/new-a6307396dfa82c88.js +0 -1
  323. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/users-fc5aafe5085739a9.js +0 -1
  324. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage-8fad54817f356e9f.js +0 -1
  325. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/overview-321f47b2dba4c780.js +0 -1
  326. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipeline-runs-bf4d162b5460acc6.js +0 -1
  327. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/edit-ed8c521227dd326e.js +0 -1
  328. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines-33d9fb90871be84c.js +0 -1
  329. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/preferences-f4ff09cfff8bad46.js +0 -1
  330. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/templates/[...slug]-04b0a55e8ad6f814.js +0 -1
  331. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/terminal-88025dd0ed3051a6.js +0 -1
  332. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/test-5b316b190ff4b226.js +0 -1
  333. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/triggers-e5e49ac3b9282aaa.js +0 -1
  334. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/version-control-b52d3a07a13452ff.js +0 -1
  335. /mage_ai/server/frontend_dist/_next/static/{4hG0vHBR7gnry-ZWEsEF3 → A7VXVGKgLQCukXcjdysDz}/_ssgManifest.js +0 -0
  336. /mage_ai/server/frontend_dist_base_path_template/_next/static/{MKDICBWwxQowqsGfMukac → wSRrM15xnvA8lOWcaque7}/_ssgManifest.js +0 -0
  337. {mage_ai-0.9.56.dist-info → mage_ai-0.9.57.dist-info}/LICENSE +0 -0
  338. {mage_ai-0.9.56.dist-info → mage_ai-0.9.57.dist-info}/WHEEL +0 -0
  339. {mage_ai-0.9.56.dist-info → mage_ai-0.9.57.dist-info}/entry_points.txt +0 -0
  340. {mage_ai-0.9.56.dist-info → mage_ai-0.9.57.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1701 @@
1
+ import asyncio
2
+ import collections
3
+ import os
4
+ import traceback
5
+ from datetime import datetime, timedelta
6
+ from typing import Any, Dict, List, Set, Tuple
7
+
8
+ import pytz
9
+ from dateutil.relativedelta import relativedelta
10
+ from sqlalchemy import desc, func
11
+
12
+ from mage_ai.data_integrations.utils.scheduler import (
13
+ clear_source_output_files,
14
+ get_extra_variables,
15
+ initialize_state_and_runs,
16
+ )
17
+ from mage_ai.data_preparation.executors.executor_factory import ExecutorFactory
18
+ from mage_ai.data_preparation.logging.logger import DictLogger
19
+ from mage_ai.data_preparation.logging.logger_manager_factory import LoggerManagerFactory
20
+ from mage_ai.data_preparation.models.constants import ExecutorType, PipelineType
21
+ from mage_ai.data_preparation.models.pipeline import Pipeline
22
+ from mage_ai.data_preparation.models.triggers import (
23
+ ScheduleInterval,
24
+ ScheduleStatus,
25
+ ScheduleType,
26
+ get_triggers_by_pipeline,
27
+ )
28
+ from mage_ai.data_preparation.repo_manager import get_repo_config
29
+ from mage_ai.data_preparation.sync.git_sync import get_sync_config
30
+ from mage_ai.orchestration.concurrency import ConcurrencyConfig, OnLimitReached
31
+ from mage_ai.orchestration.db import db_connection, safe_db_query
32
+ from mage_ai.orchestration.db.models.schedules import (
33
+ Backfill,
34
+ BlockRun,
35
+ EventMatcher,
36
+ PipelineRun,
37
+ PipelineSchedule,
38
+ )
39
+ from mage_ai.orchestration.job_manager import JobType, job_manager
40
+ from mage_ai.orchestration.metrics.pipeline_run import (
41
+ calculate_destination_metrics,
42
+ calculate_pipeline_run_metrics,
43
+ calculate_source_metrics,
44
+ )
45
+ from mage_ai.orchestration.notification.config import NotificationConfig
46
+ from mage_ai.orchestration.notification.sender import NotificationSender
47
+ from mage_ai.orchestration.utils.distributed_lock import DistributedLock
48
+ from mage_ai.orchestration.utils.git import log_git_sync, run_git_sync
49
+ from mage_ai.orchestration.utils.resources import get_compute, get_memory
50
+ from mage_ai.server.logger import Logger
51
+ from mage_ai.settings import HOSTNAME
52
+ from mage_ai.settings.platform import (
53
+ project_platform_activated,
54
+ repo_path_from_database_query_to_project_repo_path,
55
+ )
56
+ from mage_ai.settings.platform.utils import get_pipeline_from_platform
57
+ from mage_ai.settings.repo import get_repo_path
58
+ from mage_ai.shared.array import find
59
+ from mage_ai.shared.dates import compare
60
+ from mage_ai.shared.environments import get_env
61
+ from mage_ai.shared.hash import index_by, merge_dict
62
+ from mage_ai.shared.retry import retry
63
+ from mage_ai.usage_statistics.logger import UsageStatisticLogger
64
+
65
+ MEMORY_USAGE_MAXIMUM = 0.95
66
+
67
+ lock = DistributedLock()
68
+ logger = Logger().new_server_logger(__name__)
69
+
70
+
71
+ class PipelineScheduler:
72
+ def __init__(
73
+ self,
74
+ pipeline_run: PipelineRun,
75
+ ) -> None:
76
+ self.pipeline_run = pipeline_run
77
+ self.pipeline_schedule = pipeline_run.pipeline_schedule
78
+ self.pipeline = get_pipeline_from_platform(
79
+ pipeline_run.pipeline_uuid,
80
+ repo_path=self.pipeline_schedule.repo_path if self.pipeline_schedule else None,
81
+ )
82
+
83
+ # Get the list of integration stream if the pipeline is data integration pipeline
84
+ self.streams = []
85
+ if self.pipeline.type == PipelineType.INTEGRATION:
86
+ try:
87
+ self.streams = self.pipeline.streams(
88
+ self.pipeline_run.get_variables(
89
+ extra_variables=get_extra_variables(self.pipeline)
90
+ )
91
+ )
92
+ except Exception:
93
+ logger.exception(f'Fail to get streams for {pipeline_run}')
94
+ traceback.print_exc()
95
+
96
+ # Initialize the logger
97
+ self.logger_manager = LoggerManagerFactory.get_logger_manager(
98
+ pipeline_uuid=self.pipeline.uuid,
99
+ partition=self.pipeline_run.execution_partition,
100
+ repo_config=self.pipeline.repo_config,
101
+ )
102
+ self.logger = DictLogger(self.logger_manager.logger)
103
+
104
+ # Initialize the notification sender
105
+ self.notification_sender = NotificationSender(
106
+ NotificationConfig.load(
107
+ config=merge_dict(
108
+ self.pipeline.repo_config.notification_config,
109
+ self.pipeline.notification_config,
110
+ )
111
+ )
112
+ )
113
+
114
+ self.concurrency_config = ConcurrencyConfig.load(
115
+ config=self.pipeline.concurrency_config
116
+ )
117
+
118
+ # Other pipeline scheduling settings
119
+ self.allow_blocks_to_fail = (
120
+ self.pipeline_schedule.get_settings().allow_blocks_to_fail
121
+ if self.pipeline_schedule else False
122
+ )
123
+
124
+ @safe_db_query
125
+ def start(self, should_schedule: bool = True) -> bool:
126
+ """Start the pipeline run.
127
+
128
+ This method starts the pipeline run by performing necessary actions
129
+ * Update the pipeline run status
130
+ * Optionally scheduling the pipeline execution
131
+
132
+ Args:
133
+ should_schedule (bool, optional): Flag indicating whether to schedule
134
+ the pipeline execution. Defaults to True.
135
+
136
+ Returns:
137
+ bool: Whether the pipeline run is started successfully
138
+ """
139
+ if self.pipeline_run.status == PipelineRun.PipelineRunStatus.RUNNING:
140
+ return True
141
+
142
+ tags = self.build_tags()
143
+
144
+ is_integration = PipelineType.INTEGRATION == self.pipeline.type
145
+
146
+ try:
147
+ block_runs = BlockRun.query.filter(
148
+ BlockRun.pipeline_run_id == self.pipeline_run.id).all()
149
+
150
+ if len(block_runs) == 0:
151
+ if is_integration:
152
+ clear_source_output_files(
153
+ self.pipeline_run,
154
+ self.logger,
155
+ )
156
+ initialize_state_and_runs(
157
+ self.pipeline_run,
158
+ self.logger,
159
+ self.pipeline_run.get_variables(),
160
+ )
161
+ else:
162
+ self.pipeline_run.create_block_runs()
163
+ except Exception as e:
164
+ error_msg = 'Fail to initialize block runs.'
165
+ self.logger.exception(
166
+ error_msg,
167
+ **merge_dict(tags, dict(
168
+ error=e,
169
+ )),
170
+ )
171
+ self.pipeline_run.update(status=PipelineRun.PipelineRunStatus.FAILED)
172
+ self.notification_sender.send_pipeline_run_failure_message(
173
+ pipeline=self.pipeline,
174
+ pipeline_run=self.pipeline_run,
175
+ error=error_msg,
176
+ )
177
+ return False
178
+
179
+ self.pipeline_run.update(
180
+ started_at=datetime.now(tz=pytz.UTC),
181
+ status=PipelineRun.PipelineRunStatus.RUNNING,
182
+ )
183
+ if should_schedule:
184
+ self.schedule()
185
+ return True
186
+
187
+ @safe_db_query
188
+ def stop(self) -> None:
189
+ stop_pipeline_run(
190
+ self.pipeline_run,
191
+ self.pipeline,
192
+ )
193
+
194
+ @safe_db_query
195
+ def schedule(self, block_runs: List[BlockRun] = None) -> None:
196
+ if not lock.try_acquire_lock(f'pipeline_run_{self.pipeline_run.id}', timeout=10):
197
+ return
198
+
199
+ self.__run_heartbeat()
200
+
201
+ for b in self.pipeline_run.block_runs:
202
+ b.refresh()
203
+
204
+ if PipelineType.STREAMING == self.pipeline.type:
205
+ self.__schedule_pipeline()
206
+ else:
207
+ schedule = PipelineSchedule.get(
208
+ self.pipeline_run.pipeline_schedule_id,
209
+ )
210
+ backfills = schedule.backfills if schedule else []
211
+ backfill = backfills[0] if len(backfills) >= 1 else None
212
+
213
+ if backfill is not None and \
214
+ backfill.status == Backfill.Status.INITIAL and \
215
+ self.pipeline_run.status == PipelineRun.PipelineRunStatus.RUNNING:
216
+ backfill.update(
217
+ status=Backfill.Status.RUNNING,
218
+ )
219
+
220
+ if self.pipeline_run.all_blocks_completed(self.allow_blocks_to_fail):
221
+ if PipelineType.INTEGRATION == self.pipeline.type:
222
+ tags = self.build_tags()
223
+ calculate_pipeline_run_metrics(
224
+ self.pipeline_run,
225
+ logger=self.logger,
226
+ logging_tags=tags,
227
+ )
228
+
229
+ if self.pipeline_run.any_blocks_failed():
230
+ self.pipeline_run.update(
231
+ status=PipelineRun.PipelineRunStatus.FAILED,
232
+ completed_at=datetime.now(tz=pytz.UTC),
233
+ )
234
+ failed_block_runs = self.pipeline_run.failed_block_runs
235
+ error_msg = 'Failed blocks: '\
236
+ f'{", ".join([b.block_uuid for b in failed_block_runs])}.'
237
+ self.notification_sender.send_pipeline_run_failure_message(
238
+ error=error_msg,
239
+ pipeline=self.pipeline,
240
+ pipeline_run=self.pipeline_run,
241
+ )
242
+ else:
243
+ self.pipeline_run.complete()
244
+ self.notification_sender.send_pipeline_run_success_message(
245
+ pipeline=self.pipeline,
246
+ pipeline_run=self.pipeline_run,
247
+ )
248
+
249
+ asyncio.run(UsageStatisticLogger().pipeline_run_ended(self.pipeline_run))
250
+
251
+ self.logger_manager.output_logs_to_destination()
252
+
253
+ if schedule:
254
+ if backfill is not None:
255
+ """
256
+ Exclude old pipeline run retries associated with the backfill
257
+ (if a backfill's runs had failed and the backfill was retried, those
258
+ previous runs are no longer relevant) and check if the backfill's
259
+ latest pipeline runs with different execution dates were successfull.
260
+ """
261
+ latest_pipeline_runs = \
262
+ PipelineSchedule.fetch_latest_pipeline_runs_without_retries(
263
+ [backfill.pipeline_schedule_id]
264
+ )
265
+ if all([PipelineRun.PipelineRunStatus.COMPLETED == pr.status
266
+ for pr in latest_pipeline_runs]):
267
+ backfill.update(
268
+ completed_at=datetime.now(tz=pytz.UTC),
269
+ status=Backfill.Status.COMPLETED,
270
+ )
271
+ schedule.update(
272
+ status=ScheduleStatus.INACTIVE,
273
+ )
274
+ # If running once, update the schedule to inactive when pipeline run is done
275
+ elif schedule.status == ScheduleStatus.ACTIVE and \
276
+ schedule.schedule_type == ScheduleType.TIME and \
277
+ schedule.schedule_interval == ScheduleInterval.ONCE:
278
+
279
+ schedule.update(status=ScheduleStatus.INACTIVE)
280
+ elif self.__check_pipeline_run_timeout() or \
281
+ (self.pipeline_run.any_blocks_failed() and
282
+ not self.allow_blocks_to_fail):
283
+ self.pipeline_run.update(
284
+ status=PipelineRun.PipelineRunStatus.FAILED)
285
+
286
+ # Backfill status updated to "failed" if at least 1 of its pipeline runs failed
287
+ if backfill is not None:
288
+ latest_pipeline_runs = \
289
+ PipelineSchedule.fetch_latest_pipeline_runs_without_retries(
290
+ [backfill.pipeline_schedule_id]
291
+ )
292
+ if any(
293
+ [PipelineRun.PipelineRunStatus.FAILED == pr.status
294
+ for pr in latest_pipeline_runs]
295
+ ):
296
+ backfill.update(
297
+ status=Backfill.Status.FAILED,
298
+ )
299
+
300
+ asyncio.run(UsageStatisticLogger().pipeline_run_ended(self.pipeline_run))
301
+
302
+ failed_block_runs = self.pipeline_run.failed_block_runs
303
+ if len(failed_block_runs) > 0:
304
+ error_msg = 'Failed blocks: '\
305
+ f'{", ".join([b.block_uuid for b in failed_block_runs])}.'
306
+ else:
307
+ error_msg = 'Pipelien run timed out.'
308
+ self.notification_sender.send_pipeline_run_failure_message(
309
+ pipeline=self.pipeline,
310
+ pipeline_run=self.pipeline_run,
311
+ error=error_msg,
312
+ )
313
+ # Cancel block runs that are still in progress for the pipeline run.
314
+ cancel_block_runs_and_jobs(self.pipeline_run, self.pipeline)
315
+ elif PipelineType.INTEGRATION == self.pipeline.type:
316
+ self.__schedule_integration_streams(block_runs)
317
+ elif self.pipeline.run_pipeline_in_one_process:
318
+ self.__schedule_pipeline()
319
+ else:
320
+ if not self.__check_block_run_timeout():
321
+ self.__schedule_blocks(block_runs)
322
+
323
+ @safe_db_query
324
+ def on_block_complete(
325
+ self,
326
+ block_uuid: str,
327
+ metrics: Dict = None,
328
+ ) -> None:
329
+ block_run = BlockRun.get(pipeline_run_id=self.pipeline_run.id, block_uuid=block_uuid)
330
+
331
+ @retry(retries=2, delay=5)
332
+ def update_status(metrics=metrics):
333
+ metrics_prev = block_run.metrics or {}
334
+ if metrics:
335
+ metrics_prev.update(metrics)
336
+
337
+ block_run.update(
338
+ status=BlockRun.BlockRunStatus.COMPLETED,
339
+ completed_at=datetime.now(tz=pytz.UTC),
340
+ metrics=metrics_prev,
341
+ )
342
+
343
+ update_status()
344
+
345
+ self.logger.info(
346
+ f'BlockRun {block_run.id} (block_uuid: {block_uuid}) completes.',
347
+ **self.build_tags(
348
+ block_run_id=block_run.id,
349
+ block_uuid=block_run.block_uuid,
350
+ ),
351
+ )
352
+
353
+ self.pipeline_run.refresh()
354
+ if self.pipeline_run.status != PipelineRun.PipelineRunStatus.RUNNING:
355
+ return
356
+ else:
357
+ self.schedule()
358
+
359
+ @safe_db_query
360
+ def on_block_complete_without_schedule(
361
+ self,
362
+ block_uuid: str,
363
+ metrics: Dict = None,
364
+ ) -> None:
365
+ block_run = BlockRun.get(pipeline_run_id=self.pipeline_run.id, block_uuid=block_uuid)
366
+
367
+ @retry(retries=2, delay=5)
368
+ def update_status(metrics=metrics):
369
+ metrics_prev = block_run.metrics or {}
370
+ if metrics:
371
+ metrics_prev.update(metrics)
372
+
373
+ block_run.update(
374
+ status=BlockRun.BlockRunStatus.COMPLETED,
375
+ completed_at=datetime.now(tz=pytz.UTC),
376
+ metrics=metrics_prev,
377
+ )
378
+
379
+ update_status()
380
+
381
+ self.logger.info(
382
+ f'BlockRun {block_run.id} (block_uuid: {block_uuid}) completes.',
383
+ **self.build_tags(
384
+ block_run_id=block_run.id,
385
+ block_uuid=block_run.block_uuid,
386
+ ),
387
+ )
388
+
389
+ @safe_db_query
390
+ def on_block_failure(self, block_uuid: str, **kwargs) -> None:
391
+ block_run = BlockRun.get(pipeline_run_id=self.pipeline_run.id, block_uuid=block_uuid)
392
+ metrics = block_run.metrics or {}
393
+
394
+ @retry(retries=2, delay=5)
395
+ def update_status():
396
+ block_run.update(
397
+ metrics=metrics,
398
+ status=BlockRun.BlockRunStatus.FAILED,
399
+ )
400
+
401
+ error = kwargs.get('error', {})
402
+ if error:
403
+ metrics['error'] = dict(
404
+ error=str(error.get('error')),
405
+ errors=error.get('errors'),
406
+ message=error.get('message')
407
+ )
408
+
409
+ update_status()
410
+
411
+ tags = self.build_tags(
412
+ block_run_id=block_run.id,
413
+ block_uuid=block_run.block_uuid,
414
+ error=error.get('error')
415
+ )
416
+
417
+ self.logger.exception(
418
+ f'BlockRun {block_run.id} (block_uuid: {block_uuid}) failed.',
419
+ **tags,
420
+ )
421
+
422
+ if not self.allow_blocks_to_fail:
423
+ if PipelineType.INTEGRATION == self.pipeline.type:
424
+ # If a block/stream fails, stop all other streams
425
+ job_manager.kill_pipeline_run_job(self.pipeline_run.id)
426
+ for stream in self.streams:
427
+ job_manager.kill_integration_stream_job(
428
+ self.pipeline_run.id,
429
+ stream.get('tap_stream_id')
430
+ )
431
+
432
+ calculate_pipeline_run_metrics(
433
+ self.pipeline_run,
434
+ logger=self.logger,
435
+ logging_tags=tags,
436
+ )
437
+
438
+ def memory_usage_failure(self, tags: Dict = None) -> None:
439
+ if tags is None:
440
+ tags = dict()
441
+ msg = 'Memory usage across all pipeline runs has reached or exceeded the maximum '\
442
+ f'limit of {int(MEMORY_USAGE_MAXIMUM * 100)}%.'
443
+ self.logger.info(msg, tags=tags)
444
+
445
+ self.stop()
446
+
447
+ self.notification_sender.send_pipeline_run_failure_message(
448
+ pipeline=self.pipeline,
449
+ pipeline_run=self.pipeline_run,
450
+ summary=msg,
451
+ )
452
+
453
+ if PipelineType.INTEGRATION == self.pipeline.type:
454
+ calculate_pipeline_run_metrics(
455
+ self.pipeline_run,
456
+ logger=self.logger,
457
+ logging_tags=tags,
458
+ )
459
+
460
+ def build_tags(self, **kwargs):
461
+ base_tags = dict(
462
+ pipeline_run_id=self.pipeline_run.id,
463
+ pipeline_schedule_id=self.pipeline_run.pipeline_schedule_id,
464
+ pipeline_uuid=self.pipeline.uuid,
465
+ )
466
+ if HOSTNAME:
467
+ base_tags['hostname'] = HOSTNAME
468
+ return merge_dict(kwargs, base_tags)
469
+
470
+ @safe_db_query
471
+ def __check_pipeline_run_timeout(self) -> bool:
472
+ """
473
+ Check run timeout for pipeline run. The method checks if a pipeline run timeout is set
474
+ and compares to the pipeline run time. If the run time is greater than the timeout,
475
+ the run will be put into a failed state and the corresponding job is cancelled.
476
+
477
+ Returns:
478
+ bool: True if the pipeline run has timed out, False otherwise.
479
+ """
480
+ try:
481
+ pipeline_run_timeout = self.pipeline_run.pipeline_schedule.timeout
482
+
483
+ if self.pipeline_run.started_at and pipeline_run_timeout:
484
+ time_difference = datetime.now(tz=pytz.UTC).timestamp() - \
485
+ self.pipeline_run.started_at.timestamp()
486
+ if time_difference > int(pipeline_run_timeout):
487
+ self.logger.error(
488
+ f'Pipeline run timed out after {int(time_difference)} seconds',
489
+ **self.build_tags(),
490
+ )
491
+ return True
492
+ except Exception:
493
+ pass
494
+
495
+ return False
496
+
497
+ @safe_db_query
498
+ def __check_block_run_timeout(self) -> bool:
499
+ """
500
+ Check run timeout block runs. Currently only works for batch pipelines that are run
501
+ using the `__schedule_blocks` method. This method checks if a block run has exceeded
502
+ its timeout and puts the block run into a failed state and cancels the block run job.
503
+
504
+ Returns:
505
+ bool: True if any block runs have timed out, False otherwise.
506
+ """
507
+ block_runs = self.pipeline_run.running_block_runs
508
+
509
+ any_block_run_timed_out = False
510
+ for block_run in block_runs:
511
+ try:
512
+ block = self.pipeline.get_block(block_run.block_uuid)
513
+ if block and block.timeout and block_run.started_at:
514
+ time_difference = datetime.now(tz=pytz.UTC).timestamp() - \
515
+ block_run.started_at.timestamp()
516
+ if time_difference > int(block.timeout):
517
+ # Get logger from block_executor so that the error log shows up in the
518
+ # block run log file and not the pipeline run log file.
519
+ block_executor = ExecutorFactory.get_block_executor(
520
+ self.pipeline,
521
+ block.uuid,
522
+ execution_partition=self.pipeline_run.execution_partition,
523
+ )
524
+ block_executor.logger.error(
525
+ f'Block {block_run.block_uuid} timed out after ' +
526
+ f'{int(time_difference)} seconds',
527
+ **block_executor.build_tags(
528
+ block_run_id=block_run.id,
529
+ pipeline_run_id=self.pipeline_run.id,
530
+ ),
531
+ )
532
+ self.on_block_failure(block_run.block_uuid)
533
+ job_manager.kill_block_run_job(block_run.id)
534
+ any_block_run_timed_out = True
535
+ except Exception:
536
+ pass
537
+ return any_block_run_timed_out
538
+
539
+ def __schedule_blocks(self, block_runs: List[BlockRun] = None) -> None:
540
+ """Schedule the block runs for execution.
541
+
542
+ This method schedules the block runs for execution by adding jobs to the job manager.
543
+ It updates the statuses of the initial block runs and fetches any crashed block runs.
544
+ The block runs to be scheduled are determined based on the provided block runs or the
545
+ executable block runs of the pipeline run. The method adds jobs to the job manager for
546
+ each block run, invoking the `run_block` function with the pipeline run ID, block run ID,
547
+ variables, and tags as arguments.
548
+
549
+ Args:
550
+ block_runs (List[BlockRun], optional): A list of block runs. Defaults to None.
551
+
552
+ Returns:
553
+ None
554
+ """
555
+ self.pipeline_run.update_block_run_statuses(self.pipeline_run.initial_block_runs)
556
+ if block_runs is None:
557
+ block_runs_to_schedule = self.pipeline_run.executable_block_runs(
558
+ allow_blocks_to_fail=self.allow_blocks_to_fail,
559
+ )
560
+ else:
561
+ block_runs_to_schedule = block_runs
562
+ block_runs_to_schedule = \
563
+ self.__fetch_crashed_block_runs() + block_runs_to_schedule
564
+
565
+ block_run_quota = len(block_runs_to_schedule)
566
+ if self.concurrency_config.block_run_limit is not None:
567
+ queued_or_running_block_runs = self.pipeline_run.queued_or_running_block_runs
568
+ block_run_quota = self.concurrency_config.block_run_limit -\
569
+ len(queued_or_running_block_runs)
570
+ if block_run_quota <= 0:
571
+ return
572
+
573
+ for b in block_runs_to_schedule[:block_run_quota]:
574
+ tags = dict(
575
+ block_run_id=b.id,
576
+ block_uuid=b.block_uuid,
577
+ )
578
+
579
+ b.update(
580
+ status=BlockRun.BlockRunStatus.QUEUED,
581
+ )
582
+
583
+ job_manager.add_job(
584
+ JobType.BLOCK_RUN,
585
+ b.id,
586
+ run_block,
587
+ # args
588
+ self.pipeline_run.id,
589
+ b.id,
590
+ self.pipeline_run.get_variables(),
591
+ self.build_tags(**tags),
592
+ None,
593
+ None,
594
+ None,
595
+ None,
596
+ None,
597
+ None,
598
+ None,
599
+ [dict(
600
+ block_uuid=br.block_uuid,
601
+ id=br.id,
602
+ metrics=br.metrics,
603
+ status=br.status,
604
+ ) for br in self.pipeline_run.block_runs],
605
+ )
606
+
607
+ def __schedule_integration_streams(self, block_runs: List[BlockRun] = None) -> None:
608
+ """Schedule the integration streams for execution.
609
+
610
+ This method schedules the integration streams for execution by adding jobs to the job
611
+ manager. It determines the integration streams that need to be scheduled based on the
612
+ provided block runs or the pipeline run's block runs. It filters the parallel and
613
+ sequential streams to ensure only streams without corresponding integration stream jobs
614
+ are scheduled. The method generates the necessary variables and runtime arguments for the
615
+ pipeline execution. Jobs are added to the job manager to invoke the `run_integration_stream`
616
+ function for parallel streams and the `run_integration_streams` function for sequential
617
+ streams.
618
+
619
+ Args:
620
+ block_runs (List[BlockRun], optional): A list of block runs. Defaults to None.
621
+
622
+ Returns:
623
+ None
624
+ """
625
+ if block_runs is not None:
626
+ block_runs_to_schedule = block_runs
627
+ else:
628
+ # Fetch all "in progress" blocks to handle crashed block runs
629
+ block_runs_to_schedule = [
630
+ b for b in self.pipeline_run.block_runs
631
+ if b.status in [
632
+ BlockRun.BlockRunStatus.INITIAL,
633
+ BlockRun.BlockRunStatus.QUEUED,
634
+ BlockRun.BlockRunStatus.RUNNING,
635
+ ]
636
+ ]
637
+
638
+ if len(block_runs_to_schedule) > 0:
639
+ tags = self.build_tags()
640
+
641
+ block_run_stream_ids = set()
642
+ for br in block_runs_to_schedule:
643
+ stream_id = br.block_uuid.split(':')[-2]
644
+ if stream_id:
645
+ block_run_stream_ids.add(stream_id)
646
+
647
+ filtered_streams = \
648
+ [s for s in self.streams if s['tap_stream_id'] in block_run_stream_ids]
649
+ parallel_streams = list(filter(lambda s: s.get('run_in_parallel'), filtered_streams))
650
+ sequential_streams = list(filter(
651
+ lambda s: not s.get('run_in_parallel'),
652
+ filtered_streams,
653
+ ))
654
+
655
+ # Filter parallel streams so that we are only left with block runs for streams
656
+ # that do not have a corresponding integration stream job.
657
+ parallel_streams_to_schedule = []
658
+ for stream in parallel_streams:
659
+ tap_stream_id = stream.get('tap_stream_id')
660
+ if not job_manager.has_integration_stream_job(self.pipeline_run.id, tap_stream_id):
661
+ parallel_streams_to_schedule.append(stream)
662
+
663
+ # Stop scheduling if there are no streams to schedule.
664
+ if (not sequential_streams or job_manager.has_pipeline_run_job(self.pipeline_run.id)) \
665
+ and len(parallel_streams_to_schedule) == 0:
666
+ return
667
+
668
+ # Generate global variables and runtime arguments for pipeline execution.
669
+ variables = self.pipeline_run.get_variables(
670
+ extra_variables=get_extra_variables(self.pipeline),
671
+ )
672
+
673
+ pipeline_schedule = self.pipeline_run.pipeline_schedule
674
+ schedule_interval = pipeline_schedule.schedule_interval
675
+ if ScheduleType.API == pipeline_schedule.schedule_type:
676
+ execution_date = datetime.utcnow()
677
+ else:
678
+ # This will be none if trigger is API type
679
+ execution_date = pipeline_schedule.current_execution_date()
680
+
681
+ end_date = None
682
+ start_date = None
683
+ date_diff = None
684
+
685
+ if ScheduleInterval.ONCE == schedule_interval:
686
+ end_date = variables.get('_end_date')
687
+ start_date = variables.get('_start_date')
688
+ elif ScheduleInterval.HOURLY == schedule_interval:
689
+ date_diff = timedelta(hours=1)
690
+ elif ScheduleInterval.DAILY == schedule_interval:
691
+ date_diff = timedelta(days=1)
692
+ elif ScheduleInterval.WEEKLY == schedule_interval:
693
+ date_diff = timedelta(weeks=1)
694
+ elif ScheduleInterval.MONTHLY == schedule_interval:
695
+ date_diff = relativedelta(months=1)
696
+
697
+ if date_diff is not None:
698
+ end_date = (execution_date).isoformat()
699
+ start_date = (execution_date - date_diff).isoformat()
700
+
701
+ runtime_arguments = dict(
702
+ _end_date=end_date,
703
+ _execution_date=execution_date.isoformat(),
704
+ _execution_partition=self.pipeline_run.execution_partition,
705
+ _start_date=start_date,
706
+ )
707
+
708
+ executable_block_runs = [b.id for b in block_runs_to_schedule]
709
+
710
+ self.logger.info(
711
+ f'Start executing PipelineRun {self.pipeline_run.id}: '
712
+ f'pipeline {self.pipeline.uuid}',
713
+ **tags,
714
+ )
715
+
716
+ for stream in parallel_streams_to_schedule:
717
+ tap_stream_id = stream.get('tap_stream_id')
718
+ job_manager.add_job(
719
+ JobType.INTEGRATION_STREAM,
720
+ f'{self.pipeline_run.id}_{tap_stream_id}',
721
+ run_integration_stream,
722
+ # args
723
+ stream,
724
+ set(executable_block_runs),
725
+ tags,
726
+ runtime_arguments,
727
+ self.pipeline_run.id,
728
+ variables,
729
+ )
730
+
731
+ if job_manager.has_pipeline_run_job(self.pipeline_run.id) or \
732
+ len(sequential_streams) == 0:
733
+ return
734
+
735
+ job_manager.add_job(
736
+ JobType.PIPELINE_RUN,
737
+ self.pipeline_run.id,
738
+ run_integration_streams,
739
+ # args
740
+ sequential_streams,
741
+ set(executable_block_runs),
742
+ tags,
743
+ runtime_arguments,
744
+ self.pipeline_run.id,
745
+ variables,
746
+ )
747
+
748
+ def __schedule_pipeline(self) -> None:
749
+ """Schedule the pipeline run for execution.
750
+
751
+ This method schedules the pipeline run for execution by adding a job to the job manager.
752
+ If a job for the pipeline run already exists, the method returns without scheduling a new
753
+ job. The job added to the job manager invokes the `run_pipeline` function with the
754
+ pipeline run ID, variables, and tags as arguments.
755
+
756
+ Returns:
757
+ None
758
+ """
759
+ if job_manager.has_pipeline_run_job(self.pipeline_run.id):
760
+ return
761
+ self.logger.info(
762
+ f'Start a process for PipelineRun {self.pipeline_run.id}',
763
+ **self.build_tags(),
764
+ )
765
+ if PipelineType.STREAMING != self.pipeline.type:
766
+ # Reset crashed block runs to INITIAL status
767
+ self.__fetch_crashed_block_runs()
768
+ job_manager.add_job(
769
+ JobType.PIPELINE_RUN,
770
+ self.pipeline_run.id,
771
+ run_pipeline,
772
+ # args
773
+ self.pipeline_run.id,
774
+ self.pipeline_run.get_variables(),
775
+ self.build_tags(),
776
+ )
777
+
778
+ def __fetch_crashed_block_runs(self) -> None:
779
+ """Fetch and handle crashed block runs.
780
+
781
+ This method fetches the running or queued block runs of the pipeline run and checks if
782
+ their corresponding job is still active. If a job is no longer active, the status of the
783
+ block run is updated to 'INITIAL' to indicate that it needs to be re-executed. A list of
784
+ crashed block runs is returned.
785
+
786
+ Returns:
787
+ List[BlockRun]: A list of crashed block runs.
788
+ """
789
+ running_or_queued_block_runs = [b for b in self.pipeline_run.block_runs if b.status in [
790
+ BlockRun.BlockRunStatus.RUNNING,
791
+ BlockRun.BlockRunStatus.QUEUED,
792
+ ]]
793
+
794
+ crashed_runs = []
795
+ for br in running_or_queued_block_runs:
796
+ if not job_manager.has_block_run_job(br.id):
797
+ br.update(status=BlockRun.BlockRunStatus.INITIAL)
798
+ crashed_runs.append(br)
799
+
800
+ return crashed_runs
801
+
802
+ def __run_heartbeat(self) -> None:
803
+ load1, load5, load15, cpu_count = get_compute()
804
+ cpu_usage = load15 / cpu_count if cpu_count else None
805
+
806
+ free_memory, used_memory, total_memory = get_memory()
807
+ memory_usage = used_memory / total_memory if total_memory else None
808
+
809
+ tags = self.build_tags(
810
+ cpu=load15,
811
+ cpu_total=cpu_count,
812
+ cpu_usage=cpu_usage,
813
+ memory=used_memory,
814
+ memory_total=total_memory,
815
+ memory_usage=memory_usage,
816
+ )
817
+
818
+ self.logger.info(
819
+ f'Pipeline {self.pipeline.uuid} for run {self.pipeline_run.id} '
820
+ f'in schedule {self.pipeline_run.pipeline_schedule_id} is alive.',
821
+ **tags,
822
+ )
823
+
824
+ if memory_usage and memory_usage >= MEMORY_USAGE_MAXIMUM:
825
+ self.memory_usage_failure(tags)
826
+
827
+
828
+ def run_integration_streams(
829
+ streams: List[Dict],
830
+ *args,
831
+ ):
832
+ for stream in streams:
833
+ run_integration_stream(stream, *args)
834
+
835
+
836
+ def run_integration_stream(
837
+ stream: Dict,
838
+ executable_block_runs: Set[int],
839
+ tags: Dict,
840
+ runtime_arguments: Dict,
841
+ pipeline_run_id: int,
842
+ variables: Dict,
843
+ ):
844
+ """Run an integration stream within the pipeline.
845
+
846
+ This method executes an integration stream within the pipeline run. It iterates through each
847
+ stream and executes the corresponding block runs in order. It handles the configuration
848
+ and execution of the data loader, transformer blocks, and data exporter. Metrics calculation is
849
+ performed for the stream if applicable.
850
+
851
+ Args:
852
+ stream (Dict): The configuration of the integration stream.
853
+ executable_block_runs (Set[int]): A set of executable block run IDs.
854
+ tags (Dict): A dictionary of tags for logging.
855
+ runtime_arguments (Dict): A dictionary of runtime arguments.
856
+ pipeline_run_id (int): The ID of the pipeline run.
857
+ variables (Dict): A dictionary of variables.
858
+ """
859
+ pipeline_run = PipelineRun.query.get(pipeline_run_id)
860
+ pipeline_scheduler = PipelineScheduler(pipeline_run)
861
+ pipeline = pipeline_scheduler.pipeline
862
+ data_loader_block = pipeline.data_loader
863
+ data_exporter_block = pipeline.data_exporter
864
+
865
+ tap_stream_id = stream['tap_stream_id']
866
+ destination_table = stream.get('destination_table', tap_stream_id)
867
+
868
+ # all_block_runs is a list of all block runs for the pipeline run
869
+ all_block_runs = BlockRun.query.filter(BlockRun.pipeline_run_id == pipeline_run.id)
870
+ # block_runs is a list of all executable blocks runs for the pipeline run
871
+ block_runs = list(filter(lambda br: br.id in executable_block_runs, all_block_runs))
872
+
873
+ # block_runs_for_stream is a list of block runs for the specified stream
874
+ block_runs_for_stream = list(filter(lambda br: tap_stream_id in br.block_uuid, block_runs))
875
+ if len(block_runs_for_stream) == 0:
876
+ return
877
+
878
+ indexes = [0]
879
+ for br in block_runs_for_stream:
880
+ parts = br.block_uuid.split(':')
881
+ if len(parts) >= 3:
882
+ indexes.append(int(parts[2]))
883
+ max_index = max(indexes)
884
+
885
+ all_block_runs_for_stream = list(filter(
886
+ lambda br: tap_stream_id in br.block_uuid,
887
+ all_block_runs,
888
+ ))
889
+ all_indexes = [0]
890
+ for br in all_block_runs_for_stream:
891
+ # Block run block uuid foramt: "{block_uuid}:{stream_name}:{index}"
892
+ parts = br.block_uuid.split(':')
893
+ if len(parts) >= 3:
894
+ all_indexes.append(int(parts[2]))
895
+ max_index_for_stream = max(all_indexes)
896
+
897
+ # Streams can be split up into multiple parts if the source has a large amount of
898
+ # data. Loop through each part of the stream, and execute the blocks runs.
899
+ for idx in range(max_index + 1):
900
+ block_runs_in_order = []
901
+ current_block = data_loader_block
902
+
903
+ while True:
904
+ block_runs_in_order.append(
905
+ find(
906
+ lambda b: b.block_uuid ==
907
+ f'{current_block.uuid}:{tap_stream_id}:{idx}', # noqa: B023
908
+ all_block_runs,
909
+ )
910
+ )
911
+ downstream_blocks = current_block.downstream_blocks
912
+ if len(downstream_blocks) == 0:
913
+ break
914
+ current_block = downstream_blocks[0]
915
+
916
+ data_loader_uuid = f'{data_loader_block.uuid}:{tap_stream_id}:{idx}'
917
+ data_exporter_uuid = f'{data_exporter_block.uuid}:{tap_stream_id}:{idx}'
918
+
919
+ data_loader_block_run = find(
920
+ lambda b, u=data_loader_uuid: b.block_uuid == u,
921
+ all_block_runs,
922
+ )
923
+ data_exporter_block_run = find(
924
+ lambda b, u=data_exporter_uuid: b.block_uuid == u,
925
+ block_runs_for_stream,
926
+ )
927
+ if not data_loader_block_run or not data_exporter_block_run:
928
+ continue
929
+
930
+ transformer_block_runs = [br for br in block_runs_in_order if (
931
+ br.block_uuid not in [data_loader_uuid, data_exporter_uuid] and
932
+ br.id in executable_block_runs
933
+ )]
934
+
935
+ index = stream.get('index', idx)
936
+
937
+ # Create config for the block runs. This metadata will be passed into the
938
+ # block before block execution.
939
+ shared_dict = dict(
940
+ destination_table=destination_table,
941
+ index=index,
942
+ is_last_block_run=(index == max_index_for_stream),
943
+ selected_streams=[
944
+ tap_stream_id,
945
+ ],
946
+ )
947
+ block_runs_and_configs = [
948
+ (data_loader_block_run, shared_dict),
949
+ ] + [(br, shared_dict) for br in transformer_block_runs] + [
950
+ (data_exporter_block_run, shared_dict),
951
+ ]
952
+ if len(executable_block_runs) == 1 and \
953
+ data_exporter_block_run.id in executable_block_runs:
954
+ block_runs_and_configs = block_runs_and_configs[-1:]
955
+ elif data_loader_block_run.id not in executable_block_runs:
956
+ block_runs_and_configs = block_runs_and_configs[1:]
957
+
958
+ block_failed = False
959
+ for _, tup in enumerate(block_runs_and_configs):
960
+ block_run, template_runtime_configuration = tup
961
+
962
+ tags_updated = merge_dict(tags, dict(
963
+ block_run_id=block_run.id,
964
+ block_uuid=block_run.block_uuid,
965
+ ))
966
+
967
+ if block_failed:
968
+ block_run.update(
969
+ status=BlockRun.BlockRunStatus.UPSTREAM_FAILED,
970
+ )
971
+ continue
972
+
973
+ pipeline_run.refresh()
974
+ if pipeline_run.status != PipelineRun.PipelineRunStatus.RUNNING:
975
+ return
976
+
977
+ block_run.update(
978
+ started_at=datetime.now(tz=pytz.UTC),
979
+ status=BlockRun.BlockRunStatus.RUNNING,
980
+ )
981
+ pipeline_scheduler.logger.info(
982
+ f'Start a process for BlockRun {block_run.id}',
983
+ **tags_updated,
984
+ )
985
+
986
+ try:
987
+ run_block(
988
+ pipeline_run_id,
989
+ block_run.id,
990
+ variables,
991
+ tags_updated,
992
+ pipeline_type=PipelineType.INTEGRATION,
993
+ verify_output=False,
994
+ # Not retry for data integration pipeline blocks
995
+ retry_config=dict(retries=0),
996
+ runtime_arguments=runtime_arguments,
997
+ schedule_after_complete=False,
998
+ template_runtime_configuration=template_runtime_configuration,
999
+ )
1000
+ except Exception as e:
1001
+ if pipeline_scheduler.allow_blocks_to_fail:
1002
+ block_failed = True
1003
+ else:
1004
+ raise e
1005
+ else:
1006
+ tags2 = merge_dict(tags_updated.get('tags', {}), dict(
1007
+ destination_table=destination_table,
1008
+ index=index,
1009
+ stream=tap_stream_id,
1010
+ ))
1011
+ if f'{data_loader_block.uuid}:{tap_stream_id}' in block_run.block_uuid:
1012
+ calculate_source_metrics(
1013
+ pipeline_run,
1014
+ block_run,
1015
+ stream=tap_stream_id,
1016
+ logger=pipeline_scheduler.logger,
1017
+ logging_tags=merge_dict(tags_updated, dict(tags=tags2)),
1018
+ )
1019
+ elif f'{data_exporter_block.uuid}:{tap_stream_id}' in block_run.block_uuid:
1020
+ calculate_destination_metrics(
1021
+ pipeline_run,
1022
+ block_run,
1023
+ stream=tap_stream_id,
1024
+ logger=pipeline_scheduler.logger,
1025
+ logging_tags=merge_dict(tags_updated, dict(tags=tags2)),
1026
+ )
1027
+
1028
+
1029
+ def run_block(
1030
+ pipeline_run_id: int,
1031
+ block_run_id: int,
1032
+ variables: Dict,
1033
+ tags: Dict,
1034
+ input_from_output: Dict = None,
1035
+ pipeline_type: PipelineType = None,
1036
+ verify_output: bool = True,
1037
+ retry_config: Dict = None,
1038
+ runtime_arguments: Dict = None,
1039
+ schedule_after_complete: bool = False,
1040
+ template_runtime_configuration: Dict = None,
1041
+ block_run_dicts: List[Dict] = None,
1042
+ ) -> Any:
1043
+ """Execute a block within a pipeline run.
1044
+ Only run block that's with INITIAL or QUEUED status.
1045
+
1046
+ Args:
1047
+ pipeline_run_id (int): The ID of the pipeline run.
1048
+ block_run_id (int): The ID of the block run.
1049
+ variables (Dict): A dictionary of variables.
1050
+ tags (Dict): A dictionary of tags for logging.
1051
+ input_from_output (Dict, optional): A dictionary mapping input names to output names.
1052
+ pipeline_type (PipelineType, optional): The type of pipeline.
1053
+ verify_output (bool, optional): Flag indicating whether to verify the output.
1054
+ retry_config (Dict, optional): A dictionary containing retry configuration.
1055
+ runtime_arguments (Dict, optional): A dictionary of runtime arguments. Used by data
1056
+ integration pipeline.
1057
+ schedule_after_complete (bool, optional): Flag indicating whether to schedule after
1058
+ completion.
1059
+ template_runtime_configuration (Dict, optional): A dictionary of template runtime
1060
+ configuration. Used by data integration pipeline.
1061
+
1062
+ Returns:
1063
+ Any: The result of executing the block.
1064
+ """
1065
+
1066
+ pipeline_run = PipelineRun.query.get(pipeline_run_id)
1067
+ if pipeline_run.status != PipelineRun.PipelineRunStatus.RUNNING:
1068
+ return {}
1069
+
1070
+ block_run = BlockRun.query.get(block_run_id)
1071
+ if block_run.status not in [
1072
+ BlockRun.BlockRunStatus.INITIAL,
1073
+ BlockRun.BlockRunStatus.QUEUED,
1074
+ BlockRun.BlockRunStatus.RUNNING,
1075
+ ]:
1076
+ return {}
1077
+
1078
+ block_run_data = dict(status=BlockRun.BlockRunStatus.RUNNING)
1079
+ if not block_run.started_at or (block_run.metrics and not block_run.metrics.get('controller')):
1080
+ block_run_data['started_at'] = datetime.now(tz=pytz.UTC)
1081
+
1082
+ block_run.update(**block_run_data)
1083
+
1084
+ pipeline_scheduler = PipelineScheduler(pipeline_run)
1085
+ pipeline_schedule = pipeline_run.pipeline_schedule
1086
+ pipeline = pipeline_scheduler.pipeline
1087
+
1088
+ pipeline_scheduler.logger.info(
1089
+ f'Execute PipelineRun {pipeline_run.id}, BlockRun {block_run.id}: '
1090
+ f'pipeline {pipeline.uuid} block {block_run.block_uuid}',
1091
+ **tags)
1092
+
1093
+ if schedule_after_complete:
1094
+ on_complete = pipeline_scheduler.on_block_complete
1095
+ else:
1096
+ on_complete = pipeline_scheduler.on_block_complete_without_schedule
1097
+
1098
+ execution_partition = pipeline_run.execution_partition
1099
+ block_uuid = block_run.block_uuid
1100
+ block = pipeline.get_block(block_uuid)
1101
+
1102
+ if block and retry_config is None:
1103
+ repo_path = None
1104
+ if project_platform_activated() and pipeline_schedule and pipeline_schedule.repo_path:
1105
+ repo_path = pipeline_schedule.repo_path
1106
+ else:
1107
+ repo_path = get_repo_path()
1108
+
1109
+ retry_config = merge_dict(
1110
+ get_repo_config(repo_path).retry_config or dict(),
1111
+ block.retry_config or dict(),
1112
+ )
1113
+
1114
+ return ExecutorFactory.get_block_executor(
1115
+ pipeline,
1116
+ block_uuid,
1117
+ execution_partition=execution_partition,
1118
+ ).execute(
1119
+ block_run_id=block_run.id,
1120
+ global_vars=variables,
1121
+ input_from_output=input_from_output,
1122
+ on_complete=on_complete,
1123
+ on_failure=pipeline_scheduler.on_block_failure,
1124
+ pipeline_run_id=pipeline_run_id,
1125
+ retry_config=retry_config,
1126
+ runtime_arguments=runtime_arguments,
1127
+ tags=tags,
1128
+ template_runtime_configuration=template_runtime_configuration,
1129
+ verify_output=verify_output,
1130
+ block_run_dicts=block_run_dicts,
1131
+ )
1132
+
1133
+
1134
+ def run_pipeline(
1135
+ pipeline_run_id: int,
1136
+ variables: Dict,
1137
+ tags: Dict,
1138
+ allow_blocks_to_fail: bool = False,
1139
+ ):
1140
+ pipeline_run = PipelineRun.query.get(pipeline_run_id)
1141
+ pipeline_scheduler = PipelineScheduler(pipeline_run)
1142
+ pipeline = pipeline_scheduler.pipeline
1143
+ pipeline_scheduler.logger.info(f'Execute PipelineRun {pipeline_run.id}: '
1144
+ f'pipeline {pipeline.uuid}',
1145
+ **tags)
1146
+ executor_type = ExecutorFactory.get_pipeline_executor_type(pipeline)
1147
+ try:
1148
+ pipeline_run.update(executor_type=executor_type)
1149
+ except Exception:
1150
+ traceback.print_exc()
1151
+ ExecutorFactory.get_pipeline_executor(
1152
+ pipeline,
1153
+ execution_partition=pipeline_run.execution_partition,
1154
+ executor_type=executor_type,
1155
+ ).execute(
1156
+ allow_blocks_to_fail=allow_blocks_to_fail,
1157
+ global_vars=variables,
1158
+ pipeline_run_id=pipeline_run_id,
1159
+ tags=tags,
1160
+ )
1161
+
1162
+
1163
+ def configure_pipeline_run_payload(
1164
+ pipeline_schedule: PipelineSchedule,
1165
+ pipeline_type: PipelineType,
1166
+ payload: Dict = None,
1167
+ ) -> Tuple[Dict, bool]:
1168
+ if payload is None:
1169
+ payload = dict()
1170
+
1171
+ if not payload.get('variables'):
1172
+ payload['variables'] = {}
1173
+
1174
+ payload['pipeline_schedule_id'] = pipeline_schedule.id
1175
+ payload['pipeline_uuid'] = pipeline_schedule.pipeline_uuid
1176
+ execution_date = payload.get('execution_date')
1177
+ if execution_date is None:
1178
+ payload['execution_date'] = datetime.utcnow()
1179
+ elif not isinstance(execution_date, datetime):
1180
+ payload['execution_date'] = datetime.fromisoformat(execution_date)
1181
+
1182
+ # Set execution_partition in variables
1183
+ payload['variables']['execution_partition'] = \
1184
+ os.sep.join([
1185
+ str(pipeline_schedule.id),
1186
+ payload['execution_date'].strftime(format='%Y%m%dT%H%M%S_%f'),
1187
+ ])
1188
+
1189
+ is_integration = PipelineType.INTEGRATION == pipeline_type
1190
+ if is_integration:
1191
+ payload['create_block_runs'] = False
1192
+
1193
+ return payload, is_integration
1194
+
1195
+
1196
+ @safe_db_query
1197
+ def retry_pipeline_run(
1198
+ pipeline_run: Dict,
1199
+ ) -> 'PipelineRun':
1200
+ pipeline_uuid = pipeline_run['pipeline_uuid']
1201
+ pipeline = Pipeline.get(pipeline_uuid, check_if_exists=True)
1202
+
1203
+ if pipeline is None or not pipeline.is_valid_pipeline(pipeline.dir_path):
1204
+ raise Exception(f'Pipeline {pipeline_uuid} is not a valid pipeline.')
1205
+
1206
+ pipeline_schedule_id = pipeline_run['pipeline_schedule_id']
1207
+ pipeline_run_model = PipelineRun(
1208
+ id=pipeline_run['id'],
1209
+ pipeline_schedule_id=pipeline_schedule_id,
1210
+ pipeline_uuid=pipeline_uuid,
1211
+ )
1212
+ execution_date = datetime.fromisoformat(pipeline_run['execution_date'])
1213
+ new_pipeline_run = pipeline_run_model.create(
1214
+ backfill_id=pipeline_run.get('backfill_id'),
1215
+ create_block_runs=False,
1216
+ execution_date=execution_date,
1217
+ event_variables=pipeline_run.get('event_variables', {}),
1218
+ pipeline_schedule_id=pipeline_schedule_id,
1219
+ pipeline_uuid=pipeline_run_model.pipeline_uuid,
1220
+ variables=pipeline_run.get('variables', {}),
1221
+ )
1222
+ return new_pipeline_run
1223
+
1224
+
1225
+ def stop_pipeline_run(
1226
+ pipeline_run: PipelineRun,
1227
+ pipeline: Pipeline = None,
1228
+ status: PipelineRun.PipelineRunStatus = PipelineRun.PipelineRunStatus.CANCELLED,
1229
+ ) -> None:
1230
+ """Stop a pipeline run.
1231
+
1232
+ This function stops a pipeline run by cancelling the pipeline run and its
1233
+ associated block runs. If a pipeline object is provided, it also kills the jobs
1234
+ associated with the pipeline run and its integration streams if applicable.
1235
+
1236
+ Args:
1237
+ pipeline_run (PipelineRun): The pipeline run to stop.
1238
+ pipeline (Pipeline, optional): The pipeline associated with the pipeline run.
1239
+ Defaults to None.
1240
+
1241
+ Returns:
1242
+ None
1243
+ """
1244
+ if pipeline_run.status not in [PipelineRun.PipelineRunStatus.INITIAL,
1245
+ PipelineRun.PipelineRunStatus.RUNNING]:
1246
+ return
1247
+
1248
+ # Update pipeline run status to cancelled
1249
+ pipeline_run.update(status=status)
1250
+
1251
+ asyncio.run(UsageStatisticLogger().pipeline_run_ended(pipeline_run))
1252
+
1253
+ # Cancel all the block runs
1254
+ cancel_block_runs_and_jobs(pipeline_run, pipeline)
1255
+
1256
+
1257
+ def cancel_block_runs_and_jobs(
1258
+ pipeline_run: PipelineRun,
1259
+ pipeline: Pipeline = None,
1260
+ ) -> None:
1261
+ """Cancel in progress block runs and jobs for a pipeline run.
1262
+
1263
+ This function cancels blocks runs for the pipeline run. If a pipeline object
1264
+ is provided, it also kills the jobs associated with the pipeline run and its
1265
+ integration streams if applicable.
1266
+
1267
+ Args:
1268
+ pipeline_run (PipelineRun): The pipeline run to stop.
1269
+ pipeline (Pipeline, optional): The pipeline associated with the pipeline run.
1270
+ Defaults to None.
1271
+
1272
+ Returns:
1273
+ None
1274
+ """
1275
+ block_runs_to_cancel = []
1276
+ running_blocks = []
1277
+ for b in pipeline_run.block_runs:
1278
+ if b.status in [
1279
+ BlockRun.BlockRunStatus.INITIAL,
1280
+ BlockRun.BlockRunStatus.QUEUED,
1281
+ BlockRun.BlockRunStatus.RUNNING,
1282
+ ]:
1283
+ block_runs_to_cancel.append(b)
1284
+ if b.status == BlockRun.BlockRunStatus.RUNNING:
1285
+ running_blocks.append(b)
1286
+ BlockRun.batch_update_status(
1287
+ [b.id for b in block_runs_to_cancel],
1288
+ BlockRun.BlockRunStatus.CANCELLED,
1289
+ )
1290
+
1291
+ # Kill jobs for integration streams and pipeline run
1292
+ if pipeline and (
1293
+ pipeline.type in [PipelineType.INTEGRATION, PipelineType.STREAMING]
1294
+ or pipeline.run_pipeline_in_one_process
1295
+ ):
1296
+ job_manager.kill_pipeline_run_job(pipeline_run.id)
1297
+ if pipeline.type == PipelineType.INTEGRATION:
1298
+ for stream in pipeline.streams():
1299
+ job_manager.kill_integration_stream_job(
1300
+ pipeline_run.id,
1301
+ stream.get('tap_stream_id')
1302
+ )
1303
+ if pipeline_run.executor_type == ExecutorType.K8S:
1304
+ """
1305
+ TODO: Support running and cancelling pipeline runs in ECS and GCP_CLOUD_RUN executors
1306
+ """
1307
+ ExecutorFactory.get_pipeline_executor(
1308
+ pipeline,
1309
+ executor_type=pipeline_run.executor_type,
1310
+ ).cancel(pipeline_run_id=pipeline_run.id)
1311
+ else:
1312
+ for b in running_blocks:
1313
+ job_manager.kill_block_run_job(b.id)
1314
+
1315
+
1316
+ def check_sla():
1317
+ repo_pipelines = set(Pipeline.get_all_pipelines_all_projects(
1318
+ get_repo_path(),
1319
+ disable_pipelines_folder_creation=True,
1320
+ ))
1321
+ pipeline_schedules_results = PipelineSchedule.active_schedules(pipeline_uuids=repo_pipelines)
1322
+ pipeline_schedules_mapping = index_by(lambda x: x.id, pipeline_schedules_results)
1323
+
1324
+ pipeline_schedules = set([s.id for s in pipeline_schedules_results])
1325
+
1326
+ pipeline_runs = PipelineRun.in_progress_runs(pipeline_schedules)
1327
+
1328
+ if pipeline_runs:
1329
+ current_time = datetime.now(tz=pytz.UTC)
1330
+
1331
+ # TODO: combine all SLA alerts in one notification
1332
+ for pipeline_run in pipeline_runs:
1333
+ pipeline_schedule = pipeline_schedules_mapping.get(pipeline_run.pipeline_schedule_id)
1334
+ if not pipeline_schedule:
1335
+ continue
1336
+
1337
+ sla = pipeline_schedule.sla
1338
+ if not sla:
1339
+ continue
1340
+ start_date = \
1341
+ pipeline_run.execution_date \
1342
+ if pipeline_run.execution_date is not None \
1343
+ else pipeline_run.created_at
1344
+ if compare(start_date + timedelta(seconds=sla), current_time) == -1:
1345
+ # passed SLA for pipeline_run
1346
+ pipeline = Pipeline.get(pipeline_schedule.pipeline_uuid)
1347
+ notification_sender = NotificationSender(
1348
+ NotificationConfig.load(
1349
+ config=merge_dict(
1350
+ pipeline.repo_config.notification_config,
1351
+ pipeline.notification_config,
1352
+ ),
1353
+ ),
1354
+ )
1355
+ notification_sender.send_pipeline_run_sla_passed_message(
1356
+ pipeline,
1357
+ pipeline_run,
1358
+ )
1359
+
1360
+ pipeline_run.update(passed_sla=True)
1361
+
1362
+
1363
+ def schedule_all():
1364
+ """
1365
+ This method manages the scheduling and execution of pipeline runs based on specified
1366
+ concurrency and pipeline scheduling rules.
1367
+
1368
+ 1. Check whether any new pipeline runs need to be scheduled.
1369
+ 2. Group active pipeline runs by pipeline.
1370
+ 3. Run git sync if "sync_on_pipeline_run" is enabled.
1371
+ 4. For each pipeline, check whether or not any pipeline runs need to be scheduled for
1372
+ the active pipeline schedules by performing the following steps:
1373
+ 1. Loop over pipeline schedules and acquire locks.
1374
+ 2. Determine whether to schedule pipeline runs based on pipeline schedule trigger interval.
1375
+ 3. Enforce per trigger pipeline run limit and create or cancel pipeline runs.
1376
+ 4. Start pipeline runs and handle per pipeline pipeline run limit.
1377
+ 5. In active pipeline runs, check whether any block runs need to be scheduled.
1378
+
1379
+ The current limit checks can potentially run into race conditions with api or event triggered
1380
+ schedules, so that needs to be addressed at some point.
1381
+ """
1382
+ db_connection.session.expire_all()
1383
+
1384
+ repo_pipelines = set(Pipeline.get_all_pipelines_all_projects(
1385
+ get_repo_path(),
1386
+ disable_pipelines_folder_creation=True,
1387
+ ))
1388
+
1389
+ # Sync schedules from yaml file to DB
1390
+ sync_schedules(list(repo_pipelines))
1391
+
1392
+ active_pipeline_schedules = list(PipelineSchedule.active_schedules(
1393
+ pipeline_uuids=repo_pipelines,
1394
+ ))
1395
+
1396
+ backfills = Backfill.filter(pipeline_schedule_ids=[ps.id for ps in active_pipeline_schedules])
1397
+
1398
+ backfills_by_pipeline_schedule_id = index_by(
1399
+ lambda backfill: backfill.pipeline_schedule_id,
1400
+ backfills,
1401
+ )
1402
+
1403
+ active_pipeline_schedule_ids_with_landing_time_enabled = set()
1404
+ for pipeline_schedule in active_pipeline_schedules:
1405
+ if pipeline_schedule.landing_time_enabled():
1406
+ active_pipeline_schedule_ids_with_landing_time_enabled.add(pipeline_schedule.id)
1407
+
1408
+ previous_pipeline_run_by_pipeline_schedule_id = {}
1409
+ if len(active_pipeline_schedule_ids_with_landing_time_enabled) >= 1:
1410
+ row_number_column = (
1411
+ func.
1412
+ row_number().
1413
+ over(
1414
+ order_by=desc(PipelineRun.execution_date),
1415
+ partition_by=PipelineRun.pipeline_schedule_id,
1416
+ ).
1417
+ label('row_number')
1418
+ )
1419
+
1420
+ query = PipelineRun.query.filter(
1421
+ PipelineRun.pipeline_schedule_id.in_(
1422
+ active_pipeline_schedule_ids_with_landing_time_enabled,
1423
+ ),
1424
+ PipelineRun.status == PipelineRun.PipelineRunStatus.COMPLETED,
1425
+ )
1426
+ query = query.add_columns(row_number_column)
1427
+ query = query.from_self().filter(row_number_column == 1)
1428
+ for tup in query.all():
1429
+ pr, _ = tup
1430
+ previous_pipeline_run_by_pipeline_schedule_id[pr.pipeline_schedule_id] = pr
1431
+
1432
+ git_sync_result = None
1433
+ sync_config = get_sync_config()
1434
+
1435
+ active_pipeline_uuids = list(set([s.pipeline_uuid for s in active_pipeline_schedules]))
1436
+ pipeline_runs_by_pipeline = PipelineRun.active_runs_for_pipelines_grouped(active_pipeline_uuids)
1437
+
1438
+ pipeline_schedules_by_pipeline_by_repo_path = collections.defaultdict(list)
1439
+ for schedule in active_pipeline_schedules:
1440
+ repo_path = schedule.repo_path if schedule.repo_path else None
1441
+
1442
+ if repo_path not in pipeline_schedules_by_pipeline_by_repo_path:
1443
+ pipeline_schedules_by_pipeline_by_repo_path[repo_path] = {}
1444
+
1445
+ if schedule.pipeline_uuid not in pipeline_schedules_by_pipeline_by_repo_path[repo_path]:
1446
+ pipeline_schedules_by_pipeline_by_repo_path[repo_path][schedule.pipeline_uuid] = []
1447
+
1448
+ pipeline_schedules_by_pipeline_by_repo_path[repo_path][schedule.pipeline_uuid].append(
1449
+ schedule,
1450
+ )
1451
+
1452
+ """
1453
+ {
1454
+ "/home/src/repo/default_platform2/project1": "/home/src/repo/default_platform2/project1",
1455
+ "/home/src/repo/default_platform2/project2": "/home/src/repo/default_platform2/project2"
1456
+ }
1457
+ """
1458
+ pipeline_schedule_repo_paths_to_repo_path_mapping = \
1459
+ repo_path_from_database_query_to_project_repo_path('pipeline_schedules')
1460
+
1461
+ # Iterate through pipeline schedules by pipeline to handle pipeline run limits for
1462
+ # each pipeline.
1463
+ """
1464
+ {
1465
+ '/home/src/repo/default_platform2/project1': {
1466
+ 'test1': [
1467
+ <mage_ai.orchestration.db.models.schedules.PipelineSchedule object at 0xffff85a0ef80>,
1468
+ ],
1469
+ },
1470
+ '/home/src/repo/default_platform2/project2': {
1471
+ 'test2_pipeline': [
1472
+ <mage_ai.orchestration.db.models.schedules.PipelineSchedule object at 0xffff85a0f190>,
1473
+ ],
1474
+ },
1475
+ }
1476
+ """
1477
+ for pair in pipeline_schedules_by_pipeline_by_repo_path.items():
1478
+ repo_path, pipeline_schedules_by_pipeline = pair
1479
+ for pipeline_uuid, active_pipeline_schedules in pipeline_schedules_by_pipeline.items():
1480
+ pipeline_runs_to_start = []
1481
+ pipeline_runs_excluded_by_limit = []
1482
+ for pipeline_schedule in active_pipeline_schedules:
1483
+ pipeline = get_pipeline_from_platform(
1484
+ pipeline_uuid,
1485
+ repo_path=pipeline_schedule.repo_path,
1486
+ mapping=pipeline_schedule_repo_paths_to_repo_path_mapping,
1487
+ )
1488
+
1489
+ concurrency_config = ConcurrencyConfig.load(config=pipeline.concurrency_config)
1490
+
1491
+ lock_key = f'pipeline_schedule_{pipeline_schedule.id}'
1492
+ if not lock.try_acquire_lock(lock_key):
1493
+ continue
1494
+
1495
+ try:
1496
+ previous_runtimes = []
1497
+ if pipeline_schedule.id in \
1498
+ active_pipeline_schedule_ids_with_landing_time_enabled:
1499
+
1500
+ previous_pipeline_run = previous_pipeline_run_by_pipeline_schedule_id.get(
1501
+ pipeline_schedule.id,
1502
+ )
1503
+ if previous_pipeline_run:
1504
+ previous_runtimes = pipeline_schedule.runtime_history(
1505
+ pipeline_run=previous_pipeline_run,
1506
+ )
1507
+
1508
+ # Decide whether to schedule any pipeline runs
1509
+ should_schedule = pipeline_schedule.should_schedule(
1510
+ previous_runtimes=previous_runtimes,
1511
+ pipeline=pipeline,
1512
+ )
1513
+ initial_pipeline_runs = [
1514
+ r for r in pipeline_schedule.pipeline_runs
1515
+ if r.status == PipelineRun.PipelineRunStatus.INITIAL
1516
+ ]
1517
+
1518
+ if not should_schedule and not initial_pipeline_runs:
1519
+ lock.release_lock(lock_key)
1520
+ continue
1521
+
1522
+ running_pipeline_runs = [
1523
+ r for r in pipeline_schedule.pipeline_runs
1524
+ if r.status == PipelineRun.PipelineRunStatus.RUNNING
1525
+ ]
1526
+
1527
+ if should_schedule and \
1528
+ pipeline_schedule.id not in backfills_by_pipeline_schedule_id:
1529
+ # Perform git sync if "sync_on_pipeline_run" is enabled and no other git
1530
+ # sync has been run for this scheduler loop.
1531
+ if not git_sync_result and sync_config and sync_config.sync_on_pipeline_run:
1532
+ git_sync_result = run_git_sync(lock=lock, sync_config=sync_config)
1533
+
1534
+ payload = dict(
1535
+ execution_date=pipeline_schedule.current_execution_date(),
1536
+ pipeline_schedule_id=pipeline_schedule.id,
1537
+ pipeline_uuid=pipeline_uuid,
1538
+ variables=pipeline_schedule.variables,
1539
+ )
1540
+
1541
+ if len(previous_runtimes) >= 1:
1542
+ payload['metrics'] = dict(previous_runtimes=previous_runtimes)
1543
+
1544
+ if (
1545
+ pipeline_schedule.get_settings().skip_if_previous_running
1546
+ and (initial_pipeline_runs or running_pipeline_runs)
1547
+ ):
1548
+ # Cancel the pipeline run if previous pipeline runs haven't completed
1549
+ # and skip_if_previous_running is enabled
1550
+ from mage_ai.orchestration.triggers.utils import (
1551
+ create_and_cancel_pipeline_run,
1552
+ )
1553
+
1554
+ pipeline_run = create_and_cancel_pipeline_run(
1555
+ pipeline,
1556
+ pipeline_schedule,
1557
+ payload,
1558
+ message='Pipeline run limit reached... skipping this run',
1559
+ )
1560
+ else:
1561
+ payload['create_block_runs'] = False
1562
+ pipeline_run = PipelineRun.create(**payload)
1563
+ # Log Git sync status for new pipeline runs if a git sync result exists
1564
+ if git_sync_result:
1565
+ pipeline_scheduler = PipelineScheduler(pipeline_run)
1566
+ log_git_sync(
1567
+ git_sync_result,
1568
+ pipeline_scheduler.logger,
1569
+ pipeline_scheduler.build_tags(),
1570
+ )
1571
+ initial_pipeline_runs.append(pipeline_run)
1572
+
1573
+ # Enforce pipeline concurrency limit
1574
+ pipeline_run_quota = None
1575
+ if concurrency_config.pipeline_run_limit is not None:
1576
+ pipeline_run_quota = concurrency_config.pipeline_run_limit - \
1577
+ len(running_pipeline_runs)
1578
+
1579
+ if pipeline_run_quota is None:
1580
+ pipeline_run_quota = len(initial_pipeline_runs)
1581
+
1582
+ if pipeline_run_quota > 0:
1583
+ initial_pipeline_runs.sort(key=lambda x: x.execution_date)
1584
+ pipeline_runs_to_start.extend(initial_pipeline_runs[:pipeline_run_quota])
1585
+ pipeline_runs_excluded_by_limit.extend(
1586
+ initial_pipeline_runs[pipeline_run_quota:]
1587
+ )
1588
+ finally:
1589
+ lock.release_lock(lock_key)
1590
+
1591
+ pipeline_run_limit = concurrency_config.pipeline_run_limit_all_triggers
1592
+ if pipeline_run_limit is not None:
1593
+ pipeline_quota = pipeline_run_limit - len(
1594
+ pipeline_runs_by_pipeline.get(pipeline_uuid, [])
1595
+ )
1596
+ else:
1597
+ pipeline_quota = None
1598
+
1599
+ quota_filtered_runs = pipeline_runs_to_start
1600
+ if pipeline_quota is not None:
1601
+ pipeline_quota = pipeline_quota if pipeline_quota > 0 else 0
1602
+ pipeline_runs_to_start.sort(key=lambda x: x.execution_date)
1603
+ quota_filtered_runs = pipeline_runs_to_start[:pipeline_quota]
1604
+ pipeline_runs_excluded_by_limit.extend(
1605
+ pipeline_runs_to_start[pipeline_quota:]
1606
+ )
1607
+
1608
+ for r in quota_filtered_runs:
1609
+ try:
1610
+ PipelineScheduler(r).start()
1611
+ except Exception:
1612
+ logger.exception(f'Failed to start {r}')
1613
+ traceback.print_exc()
1614
+ r.update(status=PipelineRun.PipelineRunStatus.FAILED)
1615
+ continue
1616
+
1617
+ # If on_pipeline_run_limit_reached is set as SKIP, cancel the pipeline runs that
1618
+ # were not scheduled due to pipeline run limits.
1619
+ if concurrency_config.on_pipeline_run_limit_reached == OnLimitReached.SKIP:
1620
+ for r in pipeline_runs_excluded_by_limit:
1621
+ pipeline_scheduler = PipelineScheduler(r)
1622
+ pipeline_scheduler.logger.warning(
1623
+ 'Pipeline run limit reached... skipping this run',
1624
+ **pipeline_scheduler.build_tags(),
1625
+ )
1626
+ r.update(status=PipelineRun.PipelineRunStatus.CANCELLED)
1627
+
1628
+ # Schedule active pipeline runs
1629
+ active_pipeline_runs = PipelineRun.active_runs_for_pipelines(
1630
+ pipeline_uuids=repo_pipelines,
1631
+ include_block_runs=True,
1632
+ )
1633
+ logger.info(f'Active pipeline runs: {[p.id for p in active_pipeline_runs]}')
1634
+
1635
+ for r in active_pipeline_runs:
1636
+ try:
1637
+ r.refresh()
1638
+ PipelineScheduler(r).schedule()
1639
+ except Exception:
1640
+ logger.exception(f'Failed to schedule {r}')
1641
+ traceback.print_exc()
1642
+ continue
1643
+ job_manager.clean_up_jobs()
1644
+
1645
+
1646
+ def schedule_with_event(event: Dict = None):
1647
+ """
1648
+ This method manages the scheduling and execution of pipeline runs for event triggered
1649
+ schedules. The logic is relatively similar to the `schedule_all()` method.
1650
+
1651
+ 1. Evaluate event matchers and get active pipeline schedules for each matched event matcher.
1652
+ 2. Group matched pipeline schedules by pipeline.
1653
+ 3. Create a new pipeline run for each matched pipeline schedule.
1654
+
1655
+ Args:
1656
+ event (Dict): the trigger event
1657
+ """
1658
+ if event is None:
1659
+ event = dict()
1660
+ logger.info(f'Schedule with event {event}')
1661
+ all_event_matchers = EventMatcher.active_event_matchers()
1662
+
1663
+ matched_pipeline_schedules = []
1664
+ for e in all_event_matchers:
1665
+ if e.match(event):
1666
+ logger.info(f'Event matched with {e}')
1667
+ matched_pipeline_schedules.extend(e.active_pipeline_schedules())
1668
+ else:
1669
+ logger.info(f'Event not matched with {e}')
1670
+
1671
+ if len(matched_pipeline_schedules) > 0:
1672
+ from mage_ai.orchestration.triggers.utils import create_and_start_pipeline_run
1673
+ for p in matched_pipeline_schedules:
1674
+ payload = dict(
1675
+ execution_date=datetime.now(tz=pytz.UTC),
1676
+ pipeline_schedule_id=p.id,
1677
+ pipeline_uuid=p.pipeline_uuid,
1678
+ variables=merge_dict(p.variables or dict(), dict(event=event)),
1679
+ )
1680
+ create_and_start_pipeline_run(
1681
+ p.pipeline,
1682
+ p,
1683
+ payload,
1684
+ should_schedule=False,
1685
+ )
1686
+
1687
+
1688
+ def sync_schedules(pipeline_uuids: List[str]):
1689
+ trigger_configs = []
1690
+
1691
+ # Sync schedule configs from triggers.yaml to DB
1692
+ for pipeline_uuid in pipeline_uuids:
1693
+ pipeline_triggers = get_triggers_by_pipeline(pipeline_uuid)
1694
+ logger.debug(f'Sync pipeline trigger configs for {pipeline_uuid}: {pipeline_triggers}.')
1695
+ for pipeline_trigger in pipeline_triggers:
1696
+ if pipeline_trigger.envs and get_env() not in pipeline_trigger.envs:
1697
+ continue
1698
+
1699
+ trigger_configs.append(pipeline_trigger)
1700
+
1701
+ PipelineSchedule.create_or_update_batch(trigger_configs)