rucio 37.0.0rc1__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 rucio might be problematic. Click here for more details.

Files changed (487) hide show
  1. rucio/__init__.py +17 -0
  2. rucio/alembicrevision.py +15 -0
  3. rucio/cli/__init__.py +14 -0
  4. rucio/cli/account.py +216 -0
  5. rucio/cli/bin_legacy/__init__.py +13 -0
  6. rucio/cli/bin_legacy/rucio.py +2825 -0
  7. rucio/cli/bin_legacy/rucio_admin.py +2500 -0
  8. rucio/cli/command.py +272 -0
  9. rucio/cli/config.py +72 -0
  10. rucio/cli/did.py +191 -0
  11. rucio/cli/download.py +128 -0
  12. rucio/cli/lifetime_exception.py +33 -0
  13. rucio/cli/replica.py +162 -0
  14. rucio/cli/rse.py +293 -0
  15. rucio/cli/rule.py +158 -0
  16. rucio/cli/scope.py +40 -0
  17. rucio/cli/subscription.py +73 -0
  18. rucio/cli/upload.py +60 -0
  19. rucio/cli/utils.py +226 -0
  20. rucio/client/__init__.py +15 -0
  21. rucio/client/accountclient.py +432 -0
  22. rucio/client/accountlimitclient.py +183 -0
  23. rucio/client/baseclient.py +983 -0
  24. rucio/client/client.py +120 -0
  25. rucio/client/configclient.py +126 -0
  26. rucio/client/credentialclient.py +59 -0
  27. rucio/client/didclient.py +868 -0
  28. rucio/client/diracclient.py +56 -0
  29. rucio/client/downloadclient.py +1783 -0
  30. rucio/client/exportclient.py +44 -0
  31. rucio/client/fileclient.py +50 -0
  32. rucio/client/importclient.py +42 -0
  33. rucio/client/lifetimeclient.py +90 -0
  34. rucio/client/lockclient.py +109 -0
  35. rucio/client/metaconventionsclient.py +140 -0
  36. rucio/client/pingclient.py +44 -0
  37. rucio/client/replicaclient.py +452 -0
  38. rucio/client/requestclient.py +125 -0
  39. rucio/client/richclient.py +317 -0
  40. rucio/client/rseclient.py +746 -0
  41. rucio/client/ruleclient.py +294 -0
  42. rucio/client/scopeclient.py +90 -0
  43. rucio/client/subscriptionclient.py +173 -0
  44. rucio/client/touchclient.py +82 -0
  45. rucio/client/uploadclient.py +969 -0
  46. rucio/common/__init__.py +13 -0
  47. rucio/common/bittorrent.py +234 -0
  48. rucio/common/cache.py +111 -0
  49. rucio/common/checksum.py +168 -0
  50. rucio/common/client.py +122 -0
  51. rucio/common/config.py +788 -0
  52. rucio/common/constants.py +217 -0
  53. rucio/common/constraints.py +17 -0
  54. rucio/common/didtype.py +237 -0
  55. rucio/common/dumper/__init__.py +342 -0
  56. rucio/common/dumper/consistency.py +497 -0
  57. rucio/common/dumper/data_models.py +362 -0
  58. rucio/common/dumper/path_parsing.py +75 -0
  59. rucio/common/exception.py +1208 -0
  60. rucio/common/extra.py +31 -0
  61. rucio/common/logging.py +420 -0
  62. rucio/common/pcache.py +1409 -0
  63. rucio/common/plugins.py +185 -0
  64. rucio/common/policy.py +93 -0
  65. rucio/common/schema/__init__.py +200 -0
  66. rucio/common/schema/generic.py +416 -0
  67. rucio/common/schema/generic_multi_vo.py +395 -0
  68. rucio/common/stomp_utils.py +423 -0
  69. rucio/common/stopwatch.py +55 -0
  70. rucio/common/test_rucio_server.py +154 -0
  71. rucio/common/types.py +483 -0
  72. rucio/common/utils.py +1688 -0
  73. rucio/core/__init__.py +13 -0
  74. rucio/core/account.py +496 -0
  75. rucio/core/account_counter.py +236 -0
  76. rucio/core/account_limit.py +425 -0
  77. rucio/core/authentication.py +620 -0
  78. rucio/core/config.py +437 -0
  79. rucio/core/credential.py +224 -0
  80. rucio/core/did.py +3004 -0
  81. rucio/core/did_meta_plugins/__init__.py +252 -0
  82. rucio/core/did_meta_plugins/did_column_meta.py +331 -0
  83. rucio/core/did_meta_plugins/did_meta_plugin_interface.py +165 -0
  84. rucio/core/did_meta_plugins/elasticsearch_meta.py +407 -0
  85. rucio/core/did_meta_plugins/filter_engine.py +672 -0
  86. rucio/core/did_meta_plugins/json_meta.py +240 -0
  87. rucio/core/did_meta_plugins/mongo_meta.py +229 -0
  88. rucio/core/did_meta_plugins/postgres_meta.py +352 -0
  89. rucio/core/dirac.py +237 -0
  90. rucio/core/distance.py +187 -0
  91. rucio/core/exporter.py +59 -0
  92. rucio/core/heartbeat.py +363 -0
  93. rucio/core/identity.py +301 -0
  94. rucio/core/importer.py +260 -0
  95. rucio/core/lifetime_exception.py +377 -0
  96. rucio/core/lock.py +577 -0
  97. rucio/core/message.py +288 -0
  98. rucio/core/meta_conventions.py +203 -0
  99. rucio/core/monitor.py +448 -0
  100. rucio/core/naming_convention.py +195 -0
  101. rucio/core/nongrid_trace.py +136 -0
  102. rucio/core/oidc.py +1463 -0
  103. rucio/core/permission/__init__.py +161 -0
  104. rucio/core/permission/generic.py +1124 -0
  105. rucio/core/permission/generic_multi_vo.py +1144 -0
  106. rucio/core/quarantined_replica.py +224 -0
  107. rucio/core/replica.py +4483 -0
  108. rucio/core/replica_sorter.py +362 -0
  109. rucio/core/request.py +3091 -0
  110. rucio/core/rse.py +2079 -0
  111. rucio/core/rse_counter.py +185 -0
  112. rucio/core/rse_expression_parser.py +459 -0
  113. rucio/core/rse_selector.py +304 -0
  114. rucio/core/rule.py +4484 -0
  115. rucio/core/rule_grouping.py +1620 -0
  116. rucio/core/scope.py +181 -0
  117. rucio/core/subscription.py +362 -0
  118. rucio/core/topology.py +490 -0
  119. rucio/core/trace.py +375 -0
  120. rucio/core/transfer.py +1531 -0
  121. rucio/core/vo.py +169 -0
  122. rucio/core/volatile_replica.py +151 -0
  123. rucio/daemons/__init__.py +13 -0
  124. rucio/daemons/abacus/__init__.py +13 -0
  125. rucio/daemons/abacus/account.py +116 -0
  126. rucio/daemons/abacus/collection_replica.py +124 -0
  127. rucio/daemons/abacus/rse.py +117 -0
  128. rucio/daemons/atropos/__init__.py +13 -0
  129. rucio/daemons/atropos/atropos.py +242 -0
  130. rucio/daemons/auditor/__init__.py +289 -0
  131. rucio/daemons/auditor/hdfs.py +97 -0
  132. rucio/daemons/auditor/srmdumps.py +355 -0
  133. rucio/daemons/automatix/__init__.py +13 -0
  134. rucio/daemons/automatix/automatix.py +304 -0
  135. rucio/daemons/badreplicas/__init__.py +13 -0
  136. rucio/daemons/badreplicas/minos.py +322 -0
  137. rucio/daemons/badreplicas/minos_temporary_expiration.py +171 -0
  138. rucio/daemons/badreplicas/necromancer.py +196 -0
  139. rucio/daemons/bb8/__init__.py +13 -0
  140. rucio/daemons/bb8/bb8.py +353 -0
  141. rucio/daemons/bb8/common.py +759 -0
  142. rucio/daemons/bb8/nuclei_background_rebalance.py +153 -0
  143. rucio/daemons/bb8/t2_background_rebalance.py +153 -0
  144. rucio/daemons/cache/__init__.py +13 -0
  145. rucio/daemons/cache/consumer.py +133 -0
  146. rucio/daemons/common.py +405 -0
  147. rucio/daemons/conveyor/__init__.py +13 -0
  148. rucio/daemons/conveyor/common.py +562 -0
  149. rucio/daemons/conveyor/finisher.py +529 -0
  150. rucio/daemons/conveyor/poller.py +394 -0
  151. rucio/daemons/conveyor/preparer.py +205 -0
  152. rucio/daemons/conveyor/receiver.py +179 -0
  153. rucio/daemons/conveyor/stager.py +133 -0
  154. rucio/daemons/conveyor/submitter.py +403 -0
  155. rucio/daemons/conveyor/throttler.py +532 -0
  156. rucio/daemons/follower/__init__.py +13 -0
  157. rucio/daemons/follower/follower.py +101 -0
  158. rucio/daemons/hermes/__init__.py +13 -0
  159. rucio/daemons/hermes/hermes.py +534 -0
  160. rucio/daemons/judge/__init__.py +13 -0
  161. rucio/daemons/judge/cleaner.py +159 -0
  162. rucio/daemons/judge/evaluator.py +185 -0
  163. rucio/daemons/judge/injector.py +162 -0
  164. rucio/daemons/judge/repairer.py +154 -0
  165. rucio/daemons/oauthmanager/__init__.py +13 -0
  166. rucio/daemons/oauthmanager/oauthmanager.py +198 -0
  167. rucio/daemons/reaper/__init__.py +13 -0
  168. rucio/daemons/reaper/dark_reaper.py +282 -0
  169. rucio/daemons/reaper/reaper.py +739 -0
  170. rucio/daemons/replicarecoverer/__init__.py +13 -0
  171. rucio/daemons/replicarecoverer/suspicious_replica_recoverer.py +626 -0
  172. rucio/daemons/rsedecommissioner/__init__.py +13 -0
  173. rucio/daemons/rsedecommissioner/config.py +81 -0
  174. rucio/daemons/rsedecommissioner/profiles/__init__.py +24 -0
  175. rucio/daemons/rsedecommissioner/profiles/atlas.py +60 -0
  176. rucio/daemons/rsedecommissioner/profiles/generic.py +452 -0
  177. rucio/daemons/rsedecommissioner/profiles/types.py +93 -0
  178. rucio/daemons/rsedecommissioner/rse_decommissioner.py +280 -0
  179. rucio/daemons/storage/__init__.py +13 -0
  180. rucio/daemons/storage/consistency/__init__.py +13 -0
  181. rucio/daemons/storage/consistency/actions.py +848 -0
  182. rucio/daemons/tracer/__init__.py +13 -0
  183. rucio/daemons/tracer/kronos.py +511 -0
  184. rucio/daemons/transmogrifier/__init__.py +13 -0
  185. rucio/daemons/transmogrifier/transmogrifier.py +762 -0
  186. rucio/daemons/undertaker/__init__.py +13 -0
  187. rucio/daemons/undertaker/undertaker.py +137 -0
  188. rucio/db/__init__.py +13 -0
  189. rucio/db/sqla/__init__.py +52 -0
  190. rucio/db/sqla/constants.py +206 -0
  191. rucio/db/sqla/migrate_repo/__init__.py +13 -0
  192. rucio/db/sqla/migrate_repo/env.py +110 -0
  193. rucio/db/sqla/migrate_repo/versions/01eaf73ab656_add_new_rule_notification_state_progress.py +70 -0
  194. rucio/db/sqla/migrate_repo/versions/0437a40dbfd1_add_eol_at_in_rules.py +47 -0
  195. rucio/db/sqla/migrate_repo/versions/0f1adb7a599a_create_transfer_hops_table.py +59 -0
  196. rucio/db/sqla/migrate_repo/versions/102efcf145f4_added_stuck_at_column_to_rules.py +43 -0
  197. rucio/db/sqla/migrate_repo/versions/13d4f70c66a9_introduce_transfer_limits.py +91 -0
  198. rucio/db/sqla/migrate_repo/versions/140fef722e91_cleanup_distances_table.py +76 -0
  199. rucio/db/sqla/migrate_repo/versions/14ec5aeb64cf_add_request_external_host.py +43 -0
  200. rucio/db/sqla/migrate_repo/versions/156fb5b5a14_add_request_type_to_requests_idx.py +50 -0
  201. rucio/db/sqla/migrate_repo/versions/1677d4d803c8_split_rse_availability_into_multiple.py +68 -0
  202. rucio/db/sqla/migrate_repo/versions/16a0aca82e12_create_index_on_table_replicas_path.py +40 -0
  203. rucio/db/sqla/migrate_repo/versions/1803333ac20f_adding_provenance_and_phys_group.py +45 -0
  204. rucio/db/sqla/migrate_repo/versions/1a29d6a9504c_add_didtype_chck_to_requests.py +60 -0
  205. rucio/db/sqla/migrate_repo/versions/1a80adff031a_create_index_on_rules_hist_recent.py +40 -0
  206. rucio/db/sqla/migrate_repo/versions/1c45d9730ca6_increase_identity_length.py +140 -0
  207. rucio/db/sqla/migrate_repo/versions/1d1215494e95_add_quarantined_replicas_table.py +73 -0
  208. rucio/db/sqla/migrate_repo/versions/1d96f484df21_asynchronous_rules_and_rule_approval.py +74 -0
  209. rucio/db/sqla/migrate_repo/versions/1f46c5f240ac_add_bytes_column_to_bad_replicas.py +43 -0
  210. rucio/db/sqla/migrate_repo/versions/1fc15ab60d43_add_message_history_table.py +50 -0
  211. rucio/db/sqla/migrate_repo/versions/2190e703eb6e_move_rse_settings_to_rse_attributes.py +134 -0
  212. rucio/db/sqla/migrate_repo/versions/21d6b9dc9961_add_mismatch_scheme_state_to_requests.py +64 -0
  213. rucio/db/sqla/migrate_repo/versions/22cf51430c78_add_availability_column_to_table_rses.py +39 -0
  214. rucio/db/sqla/migrate_repo/versions/22d887e4ec0a_create_sources_table.py +64 -0
  215. rucio/db/sqla/migrate_repo/versions/25821a8a45a3_remove_unique_constraint_on_requests.py +51 -0
  216. rucio/db/sqla/migrate_repo/versions/25fc855625cf_added_unique_constraint_to_rules.py +41 -0
  217. rucio/db/sqla/migrate_repo/versions/269fee20dee9_add_repair_cnt_to_locks.py +43 -0
  218. rucio/db/sqla/migrate_repo/versions/271a46ea6244_add_ignore_availability_column_to_rules.py +44 -0
  219. rucio/db/sqla/migrate_repo/versions/277b5fbb41d3_switch_heartbeats_executable.py +53 -0
  220. rucio/db/sqla/migrate_repo/versions/27e3a68927fb_remove_replicas_tombstone_and_replicas_.py +38 -0
  221. rucio/db/sqla/migrate_repo/versions/2854cd9e168_added_rule_id_column.py +47 -0
  222. rucio/db/sqla/migrate_repo/versions/295289b5a800_processed_by_and__at_in_requests.py +45 -0
  223. rucio/db/sqla/migrate_repo/versions/2962ece31cf4_add_nbaccesses_column_in_the_did_table.py +45 -0
  224. rucio/db/sqla/migrate_repo/versions/2af3291ec4c_added_replicas_history_table.py +57 -0
  225. rucio/db/sqla/migrate_repo/versions/2b69addda658_add_columns_for_third_party_copy_read_.py +45 -0
  226. rucio/db/sqla/migrate_repo/versions/2b8e7bcb4783_add_config_table.py +69 -0
  227. rucio/db/sqla/migrate_repo/versions/2ba5229cb54c_add_submitted_at_to_requests_table.py +43 -0
  228. rucio/db/sqla/migrate_repo/versions/2cbee484dcf9_added_column_volume_to_rse_transfer_.py +42 -0
  229. rucio/db/sqla/migrate_repo/versions/2edee4a83846_add_source_to_requests_and_requests_.py +47 -0
  230. rucio/db/sqla/migrate_repo/versions/2eef46be23d4_change_tokens_pk.py +46 -0
  231. rucio/db/sqla/migrate_repo/versions/2f648fc909f3_index_in_rule_history_on_scope_name.py +40 -0
  232. rucio/db/sqla/migrate_repo/versions/3082b8cef557_add_naming_convention_table_and_closed_.py +67 -0
  233. rucio/db/sqla/migrate_repo/versions/30d5206e9cad_increase_oauthrequest_redirect_msg_.py +37 -0
  234. rucio/db/sqla/migrate_repo/versions/30fa38b6434e_add_index_on_service_column_in_the_message_table.py +44 -0
  235. rucio/db/sqla/migrate_repo/versions/3152492b110b_added_staging_area_column.py +77 -0
  236. rucio/db/sqla/migrate_repo/versions/32c7d2783f7e_create_bad_replicas_table.py +60 -0
  237. rucio/db/sqla/migrate_repo/versions/3345511706b8_replicas_table_pk_definition_is_in_.py +72 -0
  238. rucio/db/sqla/migrate_repo/versions/35ef10d1e11b_change_index_on_table_requests.py +42 -0
  239. rucio/db/sqla/migrate_repo/versions/379a19b5332d_create_rse_limits_table.py +65 -0
  240. rucio/db/sqla/migrate_repo/versions/384b96aa0f60_created_rule_history_tables.py +133 -0
  241. rucio/db/sqla/migrate_repo/versions/3ac1660a1a72_extend_distance_table.py +55 -0
  242. rucio/db/sqla/migrate_repo/versions/3ad36e2268b0_create_collection_replicas_updates_table.py +76 -0
  243. rucio/db/sqla/migrate_repo/versions/3c9df354071b_extend_waiting_request_state.py +60 -0
  244. rucio/db/sqla/migrate_repo/versions/3d9813fab443_add_a_new_state_lost_in_badfilesstatus.py +44 -0
  245. rucio/db/sqla/migrate_repo/versions/40ad39ce3160_add_transferred_at_to_requests_table.py +43 -0
  246. rucio/db/sqla/migrate_repo/versions/4207be2fd914_add_notification_column_to_rules.py +64 -0
  247. rucio/db/sqla/migrate_repo/versions/42db2617c364_create_index_on_requests_external_id.py +40 -0
  248. rucio/db/sqla/migrate_repo/versions/436827b13f82_added_column_activity_to_table_requests.py +43 -0
  249. rucio/db/sqla/migrate_repo/versions/44278720f774_update_requests_typ_sta_upd_idx_index.py +44 -0
  250. rucio/db/sqla/migrate_repo/versions/45378a1e76a8_create_collection_replica_table.py +78 -0
  251. rucio/db/sqla/migrate_repo/versions/469d262be19_removing_created_at_index.py +41 -0
  252. rucio/db/sqla/migrate_repo/versions/4783c1f49cb4_create_distance_table.py +59 -0
  253. rucio/db/sqla/migrate_repo/versions/49a21b4d4357_create_index_on_table_tokens.py +44 -0
  254. rucio/db/sqla/migrate_repo/versions/4a2cbedda8b9_add_source_replica_expression_column_to_.py +43 -0
  255. rucio/db/sqla/migrate_repo/versions/4a7182d9578b_added_bytes_length_accessed_at_columns.py +49 -0
  256. rucio/db/sqla/migrate_repo/versions/4bab9edd01fc_create_index_on_requests_rule_id.py +40 -0
  257. rucio/db/sqla/migrate_repo/versions/4c3a4acfe006_new_attr_account_table.py +63 -0
  258. rucio/db/sqla/migrate_repo/versions/4cf0a2e127d4_adding_transient_metadata.py +43 -0
  259. rucio/db/sqla/migrate_repo/versions/4df2c5ddabc0_remove_temporary_dids.py +55 -0
  260. rucio/db/sqla/migrate_repo/versions/50280c53117c_add_qos_class_to_rse.py +45 -0
  261. rucio/db/sqla/migrate_repo/versions/52153819589c_add_rse_id_to_replicas_table.py +43 -0
  262. rucio/db/sqla/migrate_repo/versions/52fd9f4916fa_added_activity_to_rules.py +43 -0
  263. rucio/db/sqla/migrate_repo/versions/53b479c3cb0f_fix_did_meta_table_missing_updated_at_.py +45 -0
  264. rucio/db/sqla/migrate_repo/versions/5673b4b6e843_add_wfms_metadata_to_rule_tables.py +47 -0
  265. rucio/db/sqla/migrate_repo/versions/575767d9f89_added_source_history_table.py +58 -0
  266. rucio/db/sqla/migrate_repo/versions/58bff7008037_add_started_at_to_requests.py +45 -0
  267. rucio/db/sqla/migrate_repo/versions/58c8b78301ab_rename_callback_to_message.py +106 -0
  268. rucio/db/sqla/migrate_repo/versions/5f139f77382a_added_child_rule_id_column.py +55 -0
  269. rucio/db/sqla/migrate_repo/versions/688ef1840840_adding_did_meta_table.py +50 -0
  270. rucio/db/sqla/migrate_repo/versions/6e572a9bfbf3_add_new_split_container_column_to_rules.py +47 -0
  271. rucio/db/sqla/migrate_repo/versions/70587619328_add_comment_column_for_subscriptions.py +43 -0
  272. rucio/db/sqla/migrate_repo/versions/739064d31565_remove_history_table_pks.py +41 -0
  273. rucio/db/sqla/migrate_repo/versions/7541902bf173_add_didsfollowed_and_followevents_table.py +91 -0
  274. rucio/db/sqla/migrate_repo/versions/7ec22226cdbf_new_replica_state_for_temporary_.py +72 -0
  275. rucio/db/sqla/migrate_repo/versions/810a41685bc1_added_columns_rse_transfer_limits.py +49 -0
  276. rucio/db/sqla/migrate_repo/versions/83f991c63a93_correct_rse_expression_length.py +43 -0
  277. rucio/db/sqla/migrate_repo/versions/8523998e2e76_increase_size_of_extended_attributes_.py +43 -0
  278. rucio/db/sqla/migrate_repo/versions/8ea9122275b1_adding_missing_function_based_indices.py +53 -0
  279. rucio/db/sqla/migrate_repo/versions/90f47792bb76_add_clob_payload_to_messages.py +45 -0
  280. rucio/db/sqla/migrate_repo/versions/914b8f02df38_new_table_for_lifetime_model_exceptions.py +68 -0
  281. rucio/db/sqla/migrate_repo/versions/94a5961ddbf2_add_estimator_columns.py +45 -0
  282. rucio/db/sqla/migrate_repo/versions/9a1b149a2044_add_saml_identity_type.py +94 -0
  283. rucio/db/sqla/migrate_repo/versions/9a45bc4ea66d_add_vp_table.py +54 -0
  284. rucio/db/sqla/migrate_repo/versions/9eb936a81eb1_true_is_true.py +72 -0
  285. rucio/db/sqla/migrate_repo/versions/a08fa8de1545_transfer_stats_table.py +55 -0
  286. rucio/db/sqla/migrate_repo/versions/a118956323f8_added_vo_table_and_vo_col_to_rse.py +76 -0
  287. rucio/db/sqla/migrate_repo/versions/a193a275255c_add_status_column_in_messages.py +47 -0
  288. rucio/db/sqla/migrate_repo/versions/a5f6f6e928a7_1_7_0.py +121 -0
  289. rucio/db/sqla/migrate_repo/versions/a616581ee47_added_columns_to_table_requests.py +59 -0
  290. rucio/db/sqla/migrate_repo/versions/a6eb23955c28_state_idx_non_functional.py +52 -0
  291. rucio/db/sqla/migrate_repo/versions/a74275a1ad30_added_global_quota_table.py +54 -0
  292. rucio/db/sqla/migrate_repo/versions/a93e4e47bda_heartbeats.py +64 -0
  293. rucio/db/sqla/migrate_repo/versions/ae2a56fcc89_added_comment_column_to_rules.py +49 -0
  294. rucio/db/sqla/migrate_repo/versions/b0070f3695c8_add_deletedidmeta_table.py +57 -0
  295. rucio/db/sqla/migrate_repo/versions/b4293a99f344_added_column_identity_to_table_tokens.py +43 -0
  296. rucio/db/sqla/migrate_repo/versions/b5493606bbf5_fix_primary_key_for_subscription_history.py +41 -0
  297. rucio/db/sqla/migrate_repo/versions/b7d287de34fd_removal_of_replicastate_source.py +91 -0
  298. rucio/db/sqla/migrate_repo/versions/b818052fa670_add_index_to_quarantined_replicas.py +40 -0
  299. rucio/db/sqla/migrate_repo/versions/b8caac94d7f0_add_comments_column_for_subscriptions_.py +43 -0
  300. rucio/db/sqla/migrate_repo/versions/b96a1c7e1cc4_new_bad_pfns_table_and_bad_replicas_.py +143 -0
  301. rucio/db/sqla/migrate_repo/versions/bb695f45c04_extend_request_state.py +76 -0
  302. rucio/db/sqla/migrate_repo/versions/bc68e9946deb_add_staging_timestamps_to_request.py +50 -0
  303. rucio/db/sqla/migrate_repo/versions/bf3baa1c1474_correct_pk_and_idx_for_history_tables.py +72 -0
  304. rucio/db/sqla/migrate_repo/versions/c0937668555f_add_qos_policy_map_table.py +55 -0
  305. rucio/db/sqla/migrate_repo/versions/c129ccdb2d5_add_lumiblocknr_to_dids.py +43 -0
  306. rucio/db/sqla/migrate_repo/versions/ccdbcd48206e_add_did_type_column_index_on_did_meta_.py +65 -0
  307. rucio/db/sqla/migrate_repo/versions/cebad904c4dd_new_payload_column_for_heartbeats.py +47 -0
  308. rucio/db/sqla/migrate_repo/versions/d1189a09c6e0_oauth2_0_and_jwt_feature_support_adding_.py +146 -0
  309. rucio/db/sqla/migrate_repo/versions/d23453595260_extend_request_state_for_preparer.py +104 -0
  310. rucio/db/sqla/migrate_repo/versions/d6dceb1de2d_added_purge_column_to_rules.py +44 -0
  311. rucio/db/sqla/migrate_repo/versions/d6e2c3b2cf26_remove_third_party_copy_column_from_rse.py +43 -0
  312. rucio/db/sqla/migrate_repo/versions/d91002c5841_new_account_limits_table.py +103 -0
  313. rucio/db/sqla/migrate_repo/versions/e138c364ebd0_extending_columns_for_filter_and_.py +49 -0
  314. rucio/db/sqla/migrate_repo/versions/e59300c8b179_support_for_archive.py +104 -0
  315. rucio/db/sqla/migrate_repo/versions/f1b14a8c2ac1_postgres_use_check_constraints.py +29 -0
  316. rucio/db/sqla/migrate_repo/versions/f41ffe206f37_oracle_global_temporary_tables.py +74 -0
  317. rucio/db/sqla/migrate_repo/versions/f85a2962b021_adding_transfertool_column_to_requests_.py +47 -0
  318. rucio/db/sqla/migrate_repo/versions/fa7a7d78b602_increase_refresh_token_size.py +43 -0
  319. rucio/db/sqla/migrate_repo/versions/fb28a95fe288_add_replicas_rse_id_tombstone_idx.py +37 -0
  320. rucio/db/sqla/migrate_repo/versions/fe1a65b176c9_set_third_party_copy_read_and_write_.py +43 -0
  321. rucio/db/sqla/migrate_repo/versions/fe8ea2fa9788_added_third_party_copy_column_to_rse_.py +43 -0
  322. rucio/db/sqla/models.py +1743 -0
  323. rucio/db/sqla/sautils.py +55 -0
  324. rucio/db/sqla/session.py +529 -0
  325. rucio/db/sqla/types.py +206 -0
  326. rucio/db/sqla/util.py +543 -0
  327. rucio/gateway/__init__.py +13 -0
  328. rucio/gateway/account.py +345 -0
  329. rucio/gateway/account_limit.py +363 -0
  330. rucio/gateway/authentication.py +381 -0
  331. rucio/gateway/config.py +227 -0
  332. rucio/gateway/credential.py +70 -0
  333. rucio/gateway/did.py +987 -0
  334. rucio/gateway/dirac.py +83 -0
  335. rucio/gateway/exporter.py +60 -0
  336. rucio/gateway/heartbeat.py +76 -0
  337. rucio/gateway/identity.py +189 -0
  338. rucio/gateway/importer.py +46 -0
  339. rucio/gateway/lifetime_exception.py +121 -0
  340. rucio/gateway/lock.py +153 -0
  341. rucio/gateway/meta_conventions.py +98 -0
  342. rucio/gateway/permission.py +74 -0
  343. rucio/gateway/quarantined_replica.py +79 -0
  344. rucio/gateway/replica.py +538 -0
  345. rucio/gateway/request.py +330 -0
  346. rucio/gateway/rse.py +632 -0
  347. rucio/gateway/rule.py +437 -0
  348. rucio/gateway/scope.py +100 -0
  349. rucio/gateway/subscription.py +280 -0
  350. rucio/gateway/vo.py +126 -0
  351. rucio/rse/__init__.py +96 -0
  352. rucio/rse/protocols/__init__.py +13 -0
  353. rucio/rse/protocols/bittorrent.py +194 -0
  354. rucio/rse/protocols/cache.py +111 -0
  355. rucio/rse/protocols/dummy.py +100 -0
  356. rucio/rse/protocols/gfal.py +708 -0
  357. rucio/rse/protocols/globus.py +243 -0
  358. rucio/rse/protocols/http_cache.py +82 -0
  359. rucio/rse/protocols/mock.py +123 -0
  360. rucio/rse/protocols/ngarc.py +209 -0
  361. rucio/rse/protocols/posix.py +250 -0
  362. rucio/rse/protocols/protocol.py +361 -0
  363. rucio/rse/protocols/rclone.py +365 -0
  364. rucio/rse/protocols/rfio.py +145 -0
  365. rucio/rse/protocols/srm.py +338 -0
  366. rucio/rse/protocols/ssh.py +414 -0
  367. rucio/rse/protocols/storm.py +195 -0
  368. rucio/rse/protocols/webdav.py +594 -0
  369. rucio/rse/protocols/xrootd.py +302 -0
  370. rucio/rse/rsemanager.py +881 -0
  371. rucio/rse/translation.py +260 -0
  372. rucio/tests/__init__.py +13 -0
  373. rucio/tests/common.py +280 -0
  374. rucio/tests/common_server.py +149 -0
  375. rucio/transfertool/__init__.py +13 -0
  376. rucio/transfertool/bittorrent.py +200 -0
  377. rucio/transfertool/bittorrent_driver.py +50 -0
  378. rucio/transfertool/bittorrent_driver_qbittorrent.py +134 -0
  379. rucio/transfertool/fts3.py +1600 -0
  380. rucio/transfertool/fts3_plugins.py +152 -0
  381. rucio/transfertool/globus.py +201 -0
  382. rucio/transfertool/globus_library.py +181 -0
  383. rucio/transfertool/mock.py +89 -0
  384. rucio/transfertool/transfertool.py +221 -0
  385. rucio/vcsversion.py +11 -0
  386. rucio/version.py +45 -0
  387. rucio/web/__init__.py +13 -0
  388. rucio/web/rest/__init__.py +13 -0
  389. rucio/web/rest/flaskapi/__init__.py +13 -0
  390. rucio/web/rest/flaskapi/authenticated_bp.py +27 -0
  391. rucio/web/rest/flaskapi/v1/__init__.py +13 -0
  392. rucio/web/rest/flaskapi/v1/accountlimits.py +236 -0
  393. rucio/web/rest/flaskapi/v1/accounts.py +1103 -0
  394. rucio/web/rest/flaskapi/v1/archives.py +102 -0
  395. rucio/web/rest/flaskapi/v1/auth.py +1644 -0
  396. rucio/web/rest/flaskapi/v1/common.py +426 -0
  397. rucio/web/rest/flaskapi/v1/config.py +304 -0
  398. rucio/web/rest/flaskapi/v1/credentials.py +213 -0
  399. rucio/web/rest/flaskapi/v1/dids.py +2340 -0
  400. rucio/web/rest/flaskapi/v1/dirac.py +116 -0
  401. rucio/web/rest/flaskapi/v1/export.py +75 -0
  402. rucio/web/rest/flaskapi/v1/heartbeats.py +127 -0
  403. rucio/web/rest/flaskapi/v1/identities.py +285 -0
  404. rucio/web/rest/flaskapi/v1/import.py +132 -0
  405. rucio/web/rest/flaskapi/v1/lifetime_exceptions.py +312 -0
  406. rucio/web/rest/flaskapi/v1/locks.py +358 -0
  407. rucio/web/rest/flaskapi/v1/main.py +91 -0
  408. rucio/web/rest/flaskapi/v1/meta_conventions.py +241 -0
  409. rucio/web/rest/flaskapi/v1/metrics.py +36 -0
  410. rucio/web/rest/flaskapi/v1/nongrid_traces.py +97 -0
  411. rucio/web/rest/flaskapi/v1/ping.py +88 -0
  412. rucio/web/rest/flaskapi/v1/redirect.py +366 -0
  413. rucio/web/rest/flaskapi/v1/replicas.py +1894 -0
  414. rucio/web/rest/flaskapi/v1/requests.py +998 -0
  415. rucio/web/rest/flaskapi/v1/rses.py +2250 -0
  416. rucio/web/rest/flaskapi/v1/rules.py +854 -0
  417. rucio/web/rest/flaskapi/v1/scopes.py +159 -0
  418. rucio/web/rest/flaskapi/v1/subscriptions.py +650 -0
  419. rucio/web/rest/flaskapi/v1/templates/auth_crash.html +80 -0
  420. rucio/web/rest/flaskapi/v1/templates/auth_granted.html +82 -0
  421. rucio/web/rest/flaskapi/v1/traces.py +137 -0
  422. rucio/web/rest/flaskapi/v1/types.py +20 -0
  423. rucio/web/rest/flaskapi/v1/vos.py +278 -0
  424. rucio/web/rest/main.py +18 -0
  425. rucio/web/rest/metrics.py +27 -0
  426. rucio/web/rest/ping.py +27 -0
  427. rucio-37.0.0rc1.data/data/rucio/etc/alembic.ini.template +71 -0
  428. rucio-37.0.0rc1.data/data/rucio/etc/alembic_offline.ini.template +74 -0
  429. rucio-37.0.0rc1.data/data/rucio/etc/globus-config.yml.template +5 -0
  430. rucio-37.0.0rc1.data/data/rucio/etc/ldap.cfg.template +30 -0
  431. rucio-37.0.0rc1.data/data/rucio/etc/mail_templates/rule_approval_request.tmpl +38 -0
  432. rucio-37.0.0rc1.data/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +4 -0
  433. rucio-37.0.0rc1.data/data/rucio/etc/mail_templates/rule_approved_user.tmpl +17 -0
  434. rucio-37.0.0rc1.data/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +6 -0
  435. rucio-37.0.0rc1.data/data/rucio/etc/mail_templates/rule_denied_user.tmpl +17 -0
  436. rucio-37.0.0rc1.data/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +19 -0
  437. rucio-37.0.0rc1.data/data/rucio/etc/rse-accounts.cfg.template +25 -0
  438. rucio-37.0.0rc1.data/data/rucio/etc/rucio.cfg.atlas.client.template +43 -0
  439. rucio-37.0.0rc1.data/data/rucio/etc/rucio.cfg.template +241 -0
  440. rucio-37.0.0rc1.data/data/rucio/etc/rucio_multi_vo.cfg.template +217 -0
  441. rucio-37.0.0rc1.data/data/rucio/requirements.server.txt +297 -0
  442. rucio-37.0.0rc1.data/data/rucio/tools/bootstrap.py +34 -0
  443. rucio-37.0.0rc1.data/data/rucio/tools/merge_rucio_configs.py +144 -0
  444. rucio-37.0.0rc1.data/data/rucio/tools/reset_database.py +40 -0
  445. rucio-37.0.0rc1.data/scripts/rucio +133 -0
  446. rucio-37.0.0rc1.data/scripts/rucio-abacus-account +74 -0
  447. rucio-37.0.0rc1.data/scripts/rucio-abacus-collection-replica +46 -0
  448. rucio-37.0.0rc1.data/scripts/rucio-abacus-rse +78 -0
  449. rucio-37.0.0rc1.data/scripts/rucio-admin +97 -0
  450. rucio-37.0.0rc1.data/scripts/rucio-atropos +60 -0
  451. rucio-37.0.0rc1.data/scripts/rucio-auditor +206 -0
  452. rucio-37.0.0rc1.data/scripts/rucio-automatix +50 -0
  453. rucio-37.0.0rc1.data/scripts/rucio-bb8 +57 -0
  454. rucio-37.0.0rc1.data/scripts/rucio-cache-client +141 -0
  455. rucio-37.0.0rc1.data/scripts/rucio-cache-consumer +42 -0
  456. rucio-37.0.0rc1.data/scripts/rucio-conveyor-finisher +58 -0
  457. rucio-37.0.0rc1.data/scripts/rucio-conveyor-poller +66 -0
  458. rucio-37.0.0rc1.data/scripts/rucio-conveyor-preparer +37 -0
  459. rucio-37.0.0rc1.data/scripts/rucio-conveyor-receiver +44 -0
  460. rucio-37.0.0rc1.data/scripts/rucio-conveyor-stager +76 -0
  461. rucio-37.0.0rc1.data/scripts/rucio-conveyor-submitter +139 -0
  462. rucio-37.0.0rc1.data/scripts/rucio-conveyor-throttler +104 -0
  463. rucio-37.0.0rc1.data/scripts/rucio-dark-reaper +53 -0
  464. rucio-37.0.0rc1.data/scripts/rucio-dumper +160 -0
  465. rucio-37.0.0rc1.data/scripts/rucio-follower +44 -0
  466. rucio-37.0.0rc1.data/scripts/rucio-hermes +54 -0
  467. rucio-37.0.0rc1.data/scripts/rucio-judge-cleaner +89 -0
  468. rucio-37.0.0rc1.data/scripts/rucio-judge-evaluator +137 -0
  469. rucio-37.0.0rc1.data/scripts/rucio-judge-injector +44 -0
  470. rucio-37.0.0rc1.data/scripts/rucio-judge-repairer +44 -0
  471. rucio-37.0.0rc1.data/scripts/rucio-kronos +44 -0
  472. rucio-37.0.0rc1.data/scripts/rucio-minos +53 -0
  473. rucio-37.0.0rc1.data/scripts/rucio-minos-temporary-expiration +50 -0
  474. rucio-37.0.0rc1.data/scripts/rucio-necromancer +120 -0
  475. rucio-37.0.0rc1.data/scripts/rucio-oauth-manager +63 -0
  476. rucio-37.0.0rc1.data/scripts/rucio-reaper +83 -0
  477. rucio-37.0.0rc1.data/scripts/rucio-replica-recoverer +248 -0
  478. rucio-37.0.0rc1.data/scripts/rucio-rse-decommissioner +66 -0
  479. rucio-37.0.0rc1.data/scripts/rucio-storage-consistency-actions +74 -0
  480. rucio-37.0.0rc1.data/scripts/rucio-transmogrifier +77 -0
  481. rucio-37.0.0rc1.data/scripts/rucio-undertaker +76 -0
  482. rucio-37.0.0rc1.dist-info/METADATA +92 -0
  483. rucio-37.0.0rc1.dist-info/RECORD +487 -0
  484. rucio-37.0.0rc1.dist-info/WHEEL +5 -0
  485. rucio-37.0.0rc1.dist-info/licenses/AUTHORS.rst +100 -0
  486. rucio-37.0.0rc1.dist-info/licenses/LICENSE +201 -0
  487. rucio-37.0.0rc1.dist-info/top_level.txt +1 -0
rucio/core/rule.py ADDED
@@ -0,0 +1,4484 @@
1
+ # Copyright European Organization for Nuclear Research (CERN) since 2012
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import json
16
+ import logging
17
+ from configparser import NoOptionError, NoSectionError
18
+ from copy import deepcopy
19
+ from datetime import datetime, timedelta
20
+ from os import path
21
+ from re import match
22
+ from string import Template
23
+ from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union
24
+
25
+ from dogpile.cache.api import NoValue
26
+ from sqlalchemy import delete, desc, select, update
27
+ from sqlalchemy.exc import (
28
+ IntegrityError,
29
+ NoResultFound, # https://pydoc.dev/sqlalchemy/latest/sqlalchemy.exc.NoResultFound.html
30
+ StatementError,
31
+ )
32
+ from sqlalchemy.sql import func
33
+ from sqlalchemy.sql.expression import and_, false, null, or_, true, tuple_
34
+
35
+ import rucio.core.did
36
+ import rucio.core.lock # import get_replica_locks, get_files_and_replica_locks_of_dataset
37
+ import rucio.core.replica # import get_and_lock_file_replicas, get_and_lock_file_replicas_for_dataset
38
+ from rucio.common.cache import MemcacheRegion
39
+ from rucio.common.config import config_get
40
+ from rucio.common.constants import RseAttr
41
+ from rucio.common.exception import (
42
+ DataIdentifierNotFound,
43
+ DuplicateRule,
44
+ InputValidationError,
45
+ InsufficientAccountLimit,
46
+ InsufficientTargetRSEs,
47
+ InvalidObject,
48
+ InvalidReplicationRule,
49
+ InvalidRSEExpression,
50
+ InvalidRuleWeight,
51
+ InvalidSourceReplicaExpression,
52
+ InvalidValueForKey,
53
+ ManualRuleApprovalBlocked,
54
+ ReplicationRuleCreationTemporaryFailed,
55
+ RequestNotFound,
56
+ RSEOverQuota,
57
+ RSEWriteBlocked,
58
+ RucioException,
59
+ RuleNotFound,
60
+ RuleReplaceFailed,
61
+ StagingAreaRuleRequiresLifetime,
62
+ UndefinedPolicy,
63
+ UnsupportedOperation,
64
+ )
65
+ from rucio.common.plugins import PolicyPackageAlgorithms
66
+ from rucio.common.policy import get_scratchdisk_lifetime, policy_filter
67
+ from rucio.common.schema import validate_schema
68
+ from rucio.common.types import DIDDict, InternalAccount, InternalScope, LoggerFunction, RuleDict
69
+ from rucio.common.utils import chunks, sizefmt, str_to_date
70
+ from rucio.core import account_counter, rse_counter
71
+ from rucio.core import request as request_core
72
+ from rucio.core import transfer as transfer_core
73
+ from rucio.core.account import get_account, has_account_attribute
74
+ from rucio.core.lifetime_exception import define_eol
75
+ from rucio.core.message import add_message
76
+ from rucio.core.monitor import MetricManager
77
+ from rucio.core.rse import get_rse, get_rse_name, get_rse_usage, list_rse_attributes
78
+ from rucio.core.rse_expression_parser import parse_expression
79
+ from rucio.core.rse_selector import RSESelector
80
+ from rucio.core.rule_grouping import apply_rule, apply_rule_grouping, create_transfer_dict, repair_stuck_locks_and_apply_rule_grouping
81
+ from rucio.db.sqla import filter_thread_work, models
82
+ from rucio.db.sqla.constants import OBSOLETE, BadFilesStatus, DIDAvailability, DIDReEvaluation, DIDType, LockState, ReplicaState, RequestType, RSEType, RuleGrouping, RuleNotification, RuleState
83
+ from rucio.db.sqla.session import read_session, stream_session, transactional_session
84
+
85
+ if TYPE_CHECKING:
86
+ from collections.abc import Callable, Iterator, Sequence
87
+
88
+ from sqlalchemy.orm import Session
89
+
90
+
91
+ REGION = MemcacheRegion(expiration_time=900)
92
+ METRICS = MetricManager(module=__name__)
93
+ AutoApproveT = TypeVar('AutoApproveT', bound='AutoApprove')
94
+
95
+
96
+ class AutoApprove(PolicyPackageAlgorithms):
97
+ """
98
+ Handle automatic approval algorithms for replication rules
99
+ """
100
+
101
+ _algorithm_type = 'auto_approve'
102
+
103
+ def __init__(self, rule: models.ReplicationRule, did: models.DataIdentifier, session: 'Session') -> None:
104
+ super().__init__()
105
+ self.rule = rule
106
+ self.did = did
107
+ self.session = session
108
+ self.register("default", self.default)
109
+
110
+ def evaluate(self) -> bool:
111
+ """
112
+ Evaluate the auto-approve algorithm
113
+ """
114
+ return self.get_configured_algorithm()(self.rule, self.did, self.session)
115
+
116
+ @classmethod
117
+ def get_configured_algorithm(cls: type[AutoApproveT]) -> "Callable[[models.ReplicationRule, models.DataIdentifier, Session], bool]":
118
+ """
119
+ Get the configured auto-approve algorithm
120
+ """
121
+ try:
122
+ configured_algorithm: str = str(config_get('rules', cls._algorithm_type, default='default'))
123
+ except (NoOptionError, NoSectionError, RuntimeError):
124
+ configured_algorithm = 'default'
125
+
126
+ return super()._get_one_algorithm(cls._algorithm_type, configured_algorithm)
127
+
128
+ @classmethod
129
+ def register(cls: type[AutoApproveT], name: str, fn_auto_approve: "Callable[[models.ReplicationRule, models.DataIdentifier, Session], bool]") -> None:
130
+ """
131
+ Register a new auto-approve algorithm
132
+ """
133
+ algorithm_dict = {name: fn_auto_approve}
134
+ super()._register(cls._algorithm_type, algorithm_dict)
135
+
136
+ @staticmethod
137
+ def default(rule: models.ReplicationRule, did: models.DataIdentifier, session: 'Session') -> bool:
138
+ """
139
+ Default auto-approve algorithm
140
+ """
141
+ rse_expression = rule['rse_expression']
142
+ vo = rule['account'].vo
143
+
144
+ rses = parse_expression(rse_expression, filter_={'vo': vo}, session=session)
145
+
146
+ auto_approve = False
147
+ # Block manual approval for multi-rse rules
148
+ if len(rses) > 1:
149
+ raise InvalidReplicationRule('Ask approval is not allowed for rules with multiple RSEs')
150
+ if len(rses) == 1 and not did.is_open and did.bytes is not None and did.length is not None:
151
+ # This rule can be considered for auto-approval:
152
+ rse_attr = list_rse_attributes(rse_id=rses[0]['id'], session=session)
153
+ auto_approve = False
154
+ if RseAttr.AUTO_APPROVE_BYTES in rse_attr and RseAttr.AUTO_APPROVE_FILES in rse_attr:
155
+ if did.bytes < int(rse_attr.get(RseAttr.AUTO_APPROVE_BYTES)) and did.length < int(rse_attr.get(RseAttr.AUTO_APPROVE_FILES)):
156
+ auto_approve = True
157
+ elif did.bytes < int(rse_attr.get(RseAttr.AUTO_APPROVE_BYTES, -1)):
158
+ auto_approve = True
159
+ elif did.length < int(rse_attr.get(RseAttr.AUTO_APPROVE_FILES, -1)):
160
+ auto_approve = True
161
+
162
+ return auto_approve
163
+
164
+
165
+ @transactional_session
166
+ def add_rule(
167
+ dids: 'Sequence[DIDDict]',
168
+ account: InternalAccount,
169
+ copies: int,
170
+ rse_expression: str,
171
+ grouping: Literal['ALL', 'DATASET', 'NONE'],
172
+ weight: Optional[str],
173
+ lifetime: Optional[int],
174
+ locked: bool,
175
+ subscription_id: Optional[str],
176
+ source_replica_expression: Optional[str] = None,
177
+ activity: str = 'User Subscriptions',
178
+ notify: Optional[Literal['Y', 'N', 'C', 'P']] = None,
179
+ purge_replicas: bool = False,
180
+ ignore_availability: bool = False,
181
+ comment: Optional[str] = None,
182
+ ask_approval: bool = False,
183
+ asynchronous: bool = False,
184
+ ignore_account_limit: bool = False,
185
+ priority: int = 3,
186
+ delay_injection: Optional[int] = None,
187
+ split_container: bool = False,
188
+ meta: Optional[dict[str, Any]] = None,
189
+ *,
190
+ session: "Session",
191
+ logger: LoggerFunction = logging.log
192
+ ) -> list[str]:
193
+ """
194
+ Adds a replication rule for every did in dids
195
+
196
+ :param dids: List of data identifiers.
197
+ :param account: Account issuing the rule.
198
+ :param copies: The number of replicas.
199
+ :param rse_expression: RSE expression which gets resolved into a list of rses.
200
+ :param grouping: ALL - All files will be replicated to the same RSE.
201
+ DATASET - All files in the same dataset will be replicated to the same RSE.
202
+ NONE - Files will be completely spread over all allowed RSEs without any grouping considerations at all.
203
+ :param weight: Weighting scheme to be used.
204
+ :param lifetime: The lifetime of the replication rule in seconds.
205
+ :param locked: If the rule is locked.
206
+ :param subscription_id: The subscription_id, if the rule is created by a subscription.
207
+ :param source_replica_expression: Only use replicas as source from this RSEs.
208
+ :param activity: Activity to be passed on to the conveyor.
209
+ :param notify: Notification setting of the rule ('Y', 'N', 'C'; None = 'N').
210
+ :param purge_replicas: Purge setting if a replica should be directly deleted after the rule is deleted.
211
+ :param ignore_availability: Option to ignore the availability of RSEs.
212
+ :param comment: Comment about the rule.
213
+ :param ask_approval: Ask for approval for this rule.
214
+ :param asynchronous: Create replication rule asynchronously by the judge-injector.
215
+ :param delay_injection: Create replication after 'delay' seconds. Implies asynchronous=True.
216
+ :param ignore_account_limit: Ignore quota and create the rule outside of the account limits.
217
+ :param priority: Priority of the rule and the transfers which should be submitted.
218
+ :param split_container: Should a container rule be split into individual dataset rules.
219
+ :param meta: Dictionary with metadata from the WFMS.
220
+ :param session: The database session in use.
221
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
222
+ :returns: A list of created replication rule ids.
223
+ :raises: InvalidReplicationRule, InsufficientAccountLimit, InvalidRSEExpression, DataIdentifierNotFound, ReplicationRuleCreationTemporaryFailed, InvalidRuleWeight,
224
+ StagingAreaRuleRequiresLifetime, DuplicateRule, RSEWriteBlocked, ScratchDiskLifetimeConflict, ManualRuleApprovalBlocked, RSEOverQuota
225
+ """
226
+ if copies <= 0:
227
+ raise InvalidValueForKey("The number of copies for a replication rule should be greater than 0.")
228
+
229
+ rule_ids = []
230
+
231
+ grouping_value = {'ALL': RuleGrouping.ALL, 'NONE': RuleGrouping.NONE}.get(grouping, RuleGrouping.DATASET)
232
+
233
+ with METRICS.timer('add_rule.total'):
234
+ # 1. Resolve the rse_expression into a list of RSE-ids
235
+ with METRICS.timer('add_rule.parse_rse_expression'):
236
+ vo = account.vo
237
+ if ignore_availability:
238
+ rses = parse_expression(rse_expression, filter_={'vo': vo}, session=session)
239
+ else:
240
+ rses = parse_expression(rse_expression, filter_={'vo': vo, 'availability_write': True}, session=session)
241
+
242
+ if lifetime is None: # Check if one of the rses is a staging area
243
+ if [rse for rse in rses if rse.get('staging_area', False)]:
244
+ raise StagingAreaRuleRequiresLifetime('Rules for a staging area must include a lifetime')
245
+
246
+ # Check SCRATCHDISK Policy
247
+ try:
248
+ lifetime = get_scratch_policy(account, rses, lifetime, session=session)
249
+ except UndefinedPolicy:
250
+ pass
251
+
252
+ # Auto-lock rules for TAPE rses
253
+ if not locked and lifetime is None:
254
+ if [rse for rse in rses if rse.get('rse_type', RSEType.DISK) == RSEType.TAPE]:
255
+ locked = True
256
+
257
+ # Block manual approval if RSE does not allow it
258
+ if ask_approval:
259
+ for rse in rses:
260
+ if list_rse_attributes(rse_id=rse['id'], session=session).get(RseAttr.BLOCK_MANUAL_APPROVAL, False):
261
+ raise ManualRuleApprovalBlocked()
262
+
263
+ if source_replica_expression:
264
+ try:
265
+ source_rses = parse_expression(source_replica_expression, filter_={'vo': vo}, session=session)
266
+ except InvalidRSEExpression as exc:
267
+ raise InvalidSourceReplicaExpression from exc
268
+ else:
269
+ source_rses = []
270
+
271
+ # 2. Create the rse selector
272
+ with METRICS.timer('add_rule.create_rse_selector'):
273
+ rseselector = RSESelector(account=account, rses=rses, weight=weight, copies=copies, ignore_account_limit=ask_approval or ignore_account_limit, session=session)
274
+
275
+ expires_at = datetime.utcnow() + timedelta(seconds=lifetime) if lifetime is not None else None
276
+
277
+ notify_value = {'Y': RuleNotification.YES, 'C': RuleNotification.CLOSE, 'P': RuleNotification.PROGRESS}.get(notify or '', RuleNotification.NO)
278
+
279
+ for elem in dids:
280
+ # 3. Get the did
281
+ with METRICS.timer('add_rule.get_did'):
282
+ try:
283
+ stmt = select(
284
+ models.DataIdentifier
285
+ ).where(
286
+ and_(models.DataIdentifier.scope == elem['scope'],
287
+ models.DataIdentifier.name == elem['name'])
288
+ )
289
+ did = session.execute(stmt).scalar_one()
290
+ except NoResultFound as exc:
291
+ raise DataIdentifierNotFound('Data identifier %s:%s is not valid.' % (elem['scope'], elem['name'])) from exc
292
+ except TypeError as error:
293
+ raise InvalidObject(error.args) from error # https://pylint.readthedocs.io/en/latest/user_guide/messages/warning/raise-missing-from.html
294
+
295
+ # 3.1 If the did is a constituent, relay the rule to the archive
296
+ if did.did_type == DIDType.FILE and did.constituent:
297
+ # Check if a single replica of this DID exists; Do not put rule on file if there are only replicas on TAPE
298
+ stmt = select(
299
+ func.count()
300
+ ).select_from(
301
+ models.RSEFileAssociation
302
+ ).join(
303
+ models.RSE,
304
+ models.RSEFileAssociation.rse_id == models.RSE.id
305
+ ).where(
306
+ and_(models.RSEFileAssociation.scope == did.scope,
307
+ models.RSEFileAssociation.name == did.name,
308
+ models.RSEFileAssociation.state == ReplicaState.AVAILABLE,
309
+ models.RSE.rse_type != RSEType.TAPE)
310
+ )
311
+ replica_cnt = session.execute(stmt).scalar()
312
+
313
+ if replica_cnt == 0: # Put the rule on the archive
314
+ stmt = select(
315
+ models.ConstituentAssociation
316
+ ).join(
317
+ models.RSEFileAssociation,
318
+ and_(models.ConstituentAssociation.scope == models.RSEFileAssociation.scope,
319
+ models.ConstituentAssociation.name == models.RSEFileAssociation.name)
320
+ ).where(
321
+ and_(models.ConstituentAssociation.child_scope == did.scope,
322
+ models.ConstituentAssociation.child_name == did.name)
323
+ )
324
+ archive = session.execute(stmt).scalars().first()
325
+ if archive is not None:
326
+ elem['name'] = archive.name
327
+ elem['scope'] = archive.scope
328
+ try:
329
+ stmt = select(
330
+ models.DataIdentifier
331
+ ).where(
332
+ and_(models.DataIdentifier.scope == elem['scope'],
333
+ models.DataIdentifier.name == elem['name'])
334
+ )
335
+ did = session.execute(stmt).scalar_one()
336
+ except NoResultFound as exc:
337
+ raise DataIdentifierNotFound('Data identifier %s:%s is not valid.' % (elem['scope'], elem['name'])) from exc
338
+ except TypeError as error:
339
+ raise InvalidObject(error.args) from error
340
+ else: # Put the rule on the constituent directly
341
+ pass
342
+
343
+ # 3.2 Get the lifetime
344
+ eol_at = define_eol(elem['scope'], elem['name'], rses, session=session)
345
+
346
+ # 4. Create the replication rule
347
+ with METRICS.timer('add_rule.create_rule'):
348
+ meta_json = None
349
+ if meta is not None:
350
+ try:
351
+ meta_json = json.dumps(meta)
352
+ except Exception:
353
+ meta_json = None
354
+
355
+ new_rule = models.ReplicationRule(account=account,
356
+ name=elem['name'],
357
+ scope=elem['scope'],
358
+ did_type=did.did_type,
359
+ copies=copies,
360
+ rse_expression=rse_expression,
361
+ locked=locked,
362
+ grouping=grouping_value,
363
+ expires_at=expires_at,
364
+ weight=weight,
365
+ source_replica_expression=source_replica_expression,
366
+ activity=activity,
367
+ subscription_id=subscription_id,
368
+ notification=notify_value,
369
+ purge_replicas=purge_replicas,
370
+ ignore_availability=ignore_availability,
371
+ comments=comment,
372
+ ignore_account_limit=ignore_account_limit,
373
+ priority=priority,
374
+ split_container=split_container,
375
+ meta=meta_json,
376
+ eol_at=eol_at)
377
+ try:
378
+ new_rule.save(session=session)
379
+ except IntegrityError as error:
380
+ if match('.*ORA-00001.*', str(error.args[0])) \
381
+ or match('.*IntegrityError.*UNIQUE constraint failed.*', str(error.args[0])) \
382
+ or match('.*1062.*Duplicate entry.*for key.*', str(error.args[0])) \
383
+ or match('.*IntegrityError.*duplicate key value violates unique constraint.*', error.args[0]) \
384
+ or match('.*UniqueViolation.*duplicate key value violates unique constraint.*', error.args[0]) \
385
+ or match('.*IntegrityError.*columns? .*not unique.*', error.args[0]):
386
+ raise DuplicateRule(error.args[0]) from error
387
+ raise InvalidReplicationRule(error.args[0]) from error
388
+ rule_ids.append(new_rule.id)
389
+
390
+ if ask_approval:
391
+ new_rule.state = RuleState.WAITING_APPROVAL
392
+ # Use the new rule as the argument here
393
+ auto_approver = AutoApprove(new_rule, did, session=session)
394
+ if auto_approver.evaluate():
395
+ logger(logging.DEBUG, "Auto approving rule %s", str(new_rule.id))
396
+ logger(logging.DEBUG, "Created rule %s for injection", str(new_rule.id))
397
+ approve_rule(rule_id=new_rule.id, notify_approvers=False, session=session)
398
+ continue
399
+
400
+ logger(logging.DEBUG, "Created rule %s in waiting for approval", str(new_rule.id))
401
+ __create_rule_approval_email(rule=new_rule, session=session)
402
+ continue
403
+
404
+ # Force ASYNC mode for large rules
405
+ if did.length is not None and (did.length * copies) >= 10000:
406
+ asynchronous = True
407
+ logger(logging.DEBUG, "Forced injection of rule %s", str(new_rule.id))
408
+
409
+ if asynchronous or delay_injection:
410
+ # TODO: asynchronous mode only available for closed dids (on the whole tree?)
411
+ new_rule.state = RuleState.INJECT
412
+ logger(logging.DEBUG, "Created rule %s for injection", str(new_rule.id))
413
+ if delay_injection:
414
+ new_rule.created_at = datetime.utcnow() + timedelta(seconds=delay_injection)
415
+ logger(logging.DEBUG, "Scheduled rule %s for injection on %s", str(new_rule.id), new_rule.created_at)
416
+ continue
417
+
418
+ # If Split Container is chosen, the rule will be processed ASYNC
419
+ if split_container and did.did_type == DIDType.CONTAINER:
420
+ new_rule.state = RuleState.INJECT
421
+ logger(logging.DEBUG, "Created rule %s for injection due to Split Container mode", str(new_rule.id))
422
+ continue
423
+
424
+ # 5. Apply the rule
425
+ with METRICS.timer('add_rule.apply_rule'):
426
+ try:
427
+ apply_rule(did, new_rule, [x['id'] for x in rses], [x['id'] for x in source_rses], rseselector, session=session)
428
+ except IntegrityError as error:
429
+ raise ReplicationRuleCreationTemporaryFailed(error.args[0]) from error
430
+
431
+ if new_rule.locks_stuck_cnt > 0:
432
+ new_rule.state = RuleState.STUCK
433
+ new_rule.error = 'MissingSourceReplica'
434
+ if new_rule.grouping != RuleGrouping.NONE:
435
+ stmt = update(
436
+ models.DatasetLock
437
+ ).where(
438
+ models.DatasetLock.rule_id == new_rule.id
439
+ ).values({
440
+ models.DatasetLock.state: LockState.STUCK
441
+ })
442
+ session.execute(stmt)
443
+ elif new_rule.locks_replicating_cnt == 0:
444
+ new_rule.state = RuleState.OK
445
+ if new_rule.grouping != RuleGrouping.NONE:
446
+ stmt = update(
447
+ models.DatasetLock
448
+ ).where(
449
+ models.DatasetLock.rule_id == new_rule.id
450
+ ).values({
451
+ models.DatasetLock.state: LockState.OK
452
+ })
453
+ session.execute(stmt)
454
+ session.flush()
455
+ if new_rule.notification == RuleNotification.YES:
456
+ generate_email_for_rule_ok_notification(rule=new_rule, session=session)
457
+ generate_rule_notifications(rule=new_rule, replicating_locks_before=0, session=session)
458
+ else:
459
+ new_rule.state = RuleState.REPLICATING
460
+ if new_rule.grouping != RuleGrouping.NONE:
461
+ stmt = update(
462
+ models.DatasetLock
463
+ ).where(
464
+ models.DatasetLock.rule_id == new_rule.id
465
+ ).values({
466
+ models.DatasetLock.state: LockState.REPLICATING
467
+ })
468
+ session.execute(stmt)
469
+
470
+ # Add rule to History
471
+ insert_rule_history(rule=new_rule, recent=True, longterm=True, session=session)
472
+
473
+ logger(logging.INFO, "Created rule %s [%d/%d/%d] with new algorithm for did %s:%s in state %s", str(new_rule.id), new_rule.locks_ok_cnt,
474
+ new_rule.locks_replicating_cnt, new_rule.locks_stuck_cnt, new_rule.scope, new_rule.name, str(new_rule.state))
475
+
476
+ return rule_ids
477
+
478
+
479
+ @transactional_session
480
+ def add_rules(
481
+ dids: 'Sequence[DIDDict]',
482
+ rules: 'Sequence[RuleDict]',
483
+ *,
484
+ session: "Session",
485
+ logger: LoggerFunction = logging.log
486
+ ) -> dict[tuple[InternalScope, str], list[str]]:
487
+ """
488
+ Adds a list of replication rules to every did in dids
489
+
490
+ :params dids: List of data identifiers.
491
+ :param rules: List of dictionaries defining replication rules.
492
+ {account, copies, rse_expression, grouping, weight, lifetime, locked, subscription_id, source_replica_expression, activity, notify, purge_replicas}
493
+ :param session: The database session in use.
494
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
495
+ :returns: Dictionary (scope, name) with list of created rule ids
496
+ :raises: InvalidReplicationRule, InsufficientAccountLimit, InvalidRSEExpression, DataIdentifierNotFound, ReplicationRuleCreationTemporaryFailed, InvalidRuleWeight,
497
+ StagingAreaRuleRequiresLifetime, DuplicateRule, RSEWriteBlocked, ScratchDiskLifetimeConflict, ManualRuleApprovalBlocked
498
+ """
499
+ if any(r.get("copies", 1) <= 0 for r in rules):
500
+ raise InvalidValueForKey("The number of copies for a replication rule should be greater than 0.")
501
+
502
+ with METRICS.timer('add_rules.total'):
503
+ rule_ids = {}
504
+
505
+ # 1. Fetch the RSEs from the RSE expression to restrict further queries just on these RSEs
506
+ restrict_rses = []
507
+ all_source_rses = []
508
+ with METRICS.timer('add_rules.parse_rse_expressions'):
509
+ for rule in rules:
510
+ vo = rule['account'].vo
511
+ if rule.get('ignore_availability'):
512
+ restrict_rses.extend(parse_expression(rule['rse_expression'], filter_={'vo': vo}, session=session))
513
+ else:
514
+ restrict_rses.extend(parse_expression(rule['rse_expression'], filter_={'vo': vo, 'availability_write': True}, session=session))
515
+ restrict_rses = list(set([rse['id'] for rse in restrict_rses]))
516
+
517
+ for rule in rules:
518
+ if rule.get('source_replica_expression'):
519
+ vo = rule['account'].vo
520
+ all_source_rses.extend(parse_expression(rule.get('source_replica_expression'), filter_={'vo': vo}, session=session))
521
+ all_source_rses = list(set([rse['id'] for rse in all_source_rses]))
522
+
523
+ for elem in dids:
524
+ # 2. Get the did
525
+ with METRICS.timer('add_rules.get_did'):
526
+ try:
527
+ stmt = select(
528
+ models.DataIdentifier
529
+ ).where(
530
+ and_(models.DataIdentifier.scope == elem['scope'],
531
+ models.DataIdentifier.name == elem['name'])
532
+ )
533
+ did = session.execute(stmt).scalar_one()
534
+ except NoResultFound as exc:
535
+ raise DataIdentifierNotFound('Data identifier %s:%s is not valid.' % (elem['scope'], elem['name'])) from exc
536
+ except TypeError as error:
537
+ raise InvalidObject(error.args) from error
538
+
539
+ # 2.1 If the did is a constituent, relay the rule to the archive
540
+ if did.did_type == DIDType.FILE and did.constituent: # Check if a single replica of this DID exists
541
+ stmt = select(
542
+ func.count()
543
+ ).select_from(
544
+ models.RSEFileAssociation
545
+ ).join(
546
+ models.RSE,
547
+ models.RSEFileAssociation.rse_id == models.RSE.id
548
+ ).where(
549
+ and_(models.RSEFileAssociation.scope == did.scope,
550
+ models.RSEFileAssociation.name == did.name,
551
+ models.RSEFileAssociation.state == ReplicaState.AVAILABLE,
552
+ models.RSE.rse_type != RSEType.TAPE)
553
+ )
554
+ replica_cnt = session.execute(stmt).scalar()
555
+ if replica_cnt == 0: # Put the rule on the archive
556
+ stmt = select(
557
+ models.ConstituentAssociation
558
+ ).join(
559
+ models.RSEFileAssociation,
560
+ and_(models.ConstituentAssociation.scope == models.RSEFileAssociation.scope,
561
+ models.ConstituentAssociation.name == models.RSEFileAssociation.name)
562
+ ).where(
563
+ and_(models.ConstituentAssociation.child_scope == did.scope,
564
+ models.ConstituentAssociation.child_name == did.name)
565
+ )
566
+ archive = session.execute(stmt).scalars().first()
567
+ if archive is not None:
568
+ elem['name'] = archive.name
569
+ elem['scope'] = archive.scope
570
+ try:
571
+ stmt = select(
572
+ models.DataIdentifier
573
+ ).where(
574
+ and_(models.DataIdentifier.scope == elem['scope'],
575
+ models.DataIdentifier.name == elem['name'])
576
+ )
577
+ did = session.execute(stmt).scalar_one()
578
+ except NoResultFound as exc:
579
+ raise DataIdentifierNotFound('Data identifier %s:%s is not valid.' % (elem['scope'], elem['name'])) from exc
580
+ except TypeError as error:
581
+ raise InvalidObject(error.args) from error
582
+ else: # Put the rule on the constituent directly
583
+ pass
584
+
585
+ rule_ids[(elem['scope'], elem['name'])] = []
586
+
587
+ # 3. Resolve the did into its contents
588
+ with METRICS.timer('add_rules.resolve_dids_to_locks_replicas'):
589
+ # Get all Replicas, not only the ones interesting for the rse_expression
590
+ datasetfiles, locks, replicas, source_replicas = __resolve_did_to_locks_and_replicas(did=did,
591
+ nowait=False,
592
+ restrict_rses=restrict_rses,
593
+ source_rses=all_source_rses,
594
+ session=session)
595
+
596
+ for rule in rules:
597
+ with METRICS.timer('add_rules.add_rule'):
598
+ # 4. Resolve the rse_expression into a list of RSE-ids
599
+ vo = rule['account'].vo
600
+ if rule.get('ignore_availability'):
601
+ rses = parse_expression(rule['rse_expression'], filter_={'vo': vo}, session=session)
602
+ else:
603
+ rses = parse_expression(rule['rse_expression'], filter_={'vo': vo, 'availability_write': True}, session=session)
604
+
605
+ if rule.get('lifetime', None) is None: # Check if one of the rses is a staging area
606
+ if [rse for rse in rses if rse.get('staging_area', False)]:
607
+ raise StagingAreaRuleRequiresLifetime()
608
+
609
+ # Check SCRATCHDISK Policy
610
+ try:
611
+ lifetime = get_scratch_policy(rule.get('account'), rses, rule.get('lifetime', None), session=session)
612
+ except UndefinedPolicy:
613
+ lifetime = rule.get('lifetime', None)
614
+
615
+ rule['lifetime'] = lifetime
616
+
617
+ # 4.5 Get the lifetime
618
+ eol_at = define_eol(did.scope, did.name, rses, session=session)
619
+
620
+ # Auto-lock rules for TAPE rses
621
+ if not rule.get('locked', False) and rule.get('lifetime', None) is None:
622
+ if [rse for rse in rses if rse.get('rse_type', RSEType.DISK) == RSEType.TAPE]:
623
+ rule['locked'] = True
624
+
625
+ # Block manual approval if RSE does not allow it
626
+ if rule.get('ask_approval', False):
627
+ for rse in rses:
628
+ if list_rse_attributes(rse_id=rse['id'], session=session).get(RseAttr.BLOCK_MANUAL_APPROVAL, False):
629
+ raise ManualRuleApprovalBlocked()
630
+
631
+ if rule.get('source_replica_expression'):
632
+ source_rses = parse_expression(rule.get('source_replica_expression'), filter_={'vo': vo}, session=session)
633
+ else:
634
+ source_rses = []
635
+
636
+ # 5. Create the RSE selector
637
+ with METRICS.timer('add_rules.create_rse_selector'):
638
+ rseselector = RSESelector(account=rule['account'], rses=rses, weight=rule.get('weight'), copies=rule['copies'], ignore_account_limit=rule.get('ask_approval', False), session=session)
639
+
640
+ # 4. Create the replication rule
641
+ with METRICS.timer('add_rules.create_rule'):
642
+ grouping = {'ALL': RuleGrouping.ALL, 'NONE': RuleGrouping.NONE}.get(str(rule.get('grouping')), RuleGrouping.DATASET)
643
+
644
+ rule_lifetime: Optional[int] = rule.get('lifetime')
645
+ expires_at: Optional[datetime] = datetime.utcnow() + timedelta(seconds=rule_lifetime) if rule_lifetime is not None else None
646
+
647
+ notify = {'Y': RuleNotification.YES, 'C': RuleNotification.CLOSE, 'P': RuleNotification.PROGRESS, None: RuleNotification.NO}.get(rule.get('notify'))
648
+
649
+ if rule.get('meta') is not None:
650
+ try:
651
+ meta = json.dumps(rule.get('meta'))
652
+ except Exception:
653
+ meta = None
654
+ else:
655
+ meta = None
656
+
657
+ new_rule = models.ReplicationRule(account=rule['account'],
658
+ name=did.name,
659
+ scope=did.scope,
660
+ did_type=did.did_type,
661
+ copies=rule['copies'],
662
+ rse_expression=rule['rse_expression'],
663
+ locked=rule.get('locked'),
664
+ grouping=grouping,
665
+ expires_at=expires_at,
666
+ weight=rule.get('weight'),
667
+ source_replica_expression=rule.get('source_replica_expression'),
668
+ activity=rule.get('activity'),
669
+ subscription_id=rule.get('subscription_id'),
670
+ notification=notify,
671
+ purge_replicas=rule.get('purge_replicas', False),
672
+ ignore_availability=rule.get('ignore_availability', False),
673
+ comments=rule.get('comment', None),
674
+ priority=rule.get('priority', 3),
675
+ split_container=rule.get('split_container', False),
676
+ meta=meta,
677
+ eol_at=eol_at)
678
+ try:
679
+ new_rule.save(session=session)
680
+ except IntegrityError as error:
681
+ if match('.*ORA-00001.*', str(error.args[0])):
682
+ raise DuplicateRule(error.args[0]) from error
683
+ elif str(error.args[0]) == '(IntegrityError) UNIQUE constraint failed: rules.scope, rules.name, rules.account, rules.rse_expression, rules.copies':
684
+ raise DuplicateRule(error.args[0]) from error
685
+ raise InvalidReplicationRule(error.args[0]) from error
686
+
687
+ rule_ids[(did.scope, did.name)].append(new_rule.id)
688
+
689
+ if rule.get('ask_approval', False):
690
+ new_rule.state = RuleState.WAITING_APPROVAL
691
+ # Block manual approval for multi-rse rules
692
+ if len(rses) > 1:
693
+ raise InvalidReplicationRule('Ask approval is not allowed for rules with multiple RSEs')
694
+ if len(rses) == 1 and not did.is_open and did.bytes is not None and did.length is not None:
695
+ # This rule can be considered for auto-approval:
696
+ rse_attr = list_rse_attributes(rse_id=rses[0]['id'], session=session)
697
+ auto_approve = False
698
+ if RseAttr.AUTO_APPROVE_BYTES in rse_attr and RseAttr.AUTO_APPROVE_FILES in rse_attr:
699
+ if did.bytes < int(rse_attr.get(RseAttr.AUTO_APPROVE_BYTES)) and did.length < int(rse_attr.get(RseAttr.AUTO_APPROVE_BYTES)):
700
+ auto_approve = True
701
+ elif did.bytes < int(rse_attr.get(RseAttr.AUTO_APPROVE_BYTES, -1)):
702
+ auto_approve = True
703
+ elif did.length < int(rse_attr.get(RseAttr.AUTO_APPROVE_FILES, -1)):
704
+ auto_approve = True
705
+ if auto_approve:
706
+ logger(logging.DEBUG, "Auto approving rule %s", str(new_rule.id))
707
+ logger(logging.DEBUG, "Created rule %s for injection", str(new_rule.id))
708
+ approve_rule(rule_id=new_rule.id, notify_approvers=False, session=session)
709
+ continue
710
+ logger(logging.DEBUG, "Created rule %s in waiting for approval", str(new_rule.id))
711
+ __create_rule_approval_email(rule=new_rule, session=session)
712
+ continue
713
+
714
+ delay_injection = rule.get('delay_injection')
715
+ if rule.get('asynchronous', False) or delay_injection:
716
+ new_rule.state = RuleState.INJECT
717
+ logger(logging.DEBUG, "Created rule %s for injection", str(new_rule.id))
718
+ if delay_injection:
719
+ new_rule.created_at = datetime.utcnow() + timedelta(seconds=delay_injection)
720
+ logger(logging.DEBUG, "Scheduled rule %s for injection on %s", (str(new_rule.id), new_rule.created_at))
721
+ continue
722
+
723
+ if rule.get('split_container', False) and did.did_type == DIDType.CONTAINER:
724
+ new_rule.state = RuleState.INJECT
725
+ logger(logging.DEBUG, "Created rule %s for injection due to Split Container mode", str(new_rule.id))
726
+ continue
727
+
728
+ # 5. Apply the replication rule to create locks, replicas and transfers
729
+ with METRICS.timer('add_rules.create_locks_replicas_transfers'):
730
+ try:
731
+ __create_locks_replicas_transfers(datasetfiles=datasetfiles,
732
+ locks=locks,
733
+ replicas=replicas,
734
+ source_replicas=source_replicas,
735
+ rseselector=rseselector,
736
+ rule=new_rule,
737
+ preferred_rse_ids=[],
738
+ source_rses=[rse['id'] for rse in source_rses],
739
+ session=session)
740
+ except IntegrityError as error:
741
+ raise ReplicationRuleCreationTemporaryFailed(error.args[0]) from error
742
+
743
+ if new_rule.locks_stuck_cnt > 0:
744
+ new_rule.state = RuleState.STUCK
745
+ new_rule.error = 'MissingSourceReplica'
746
+ if new_rule.grouping != RuleGrouping.NONE:
747
+ stmt = update(
748
+ models.DatasetLock
749
+ ).where(
750
+ models.DatasetLock.rule_id == new_rule.id
751
+ ).values({
752
+ models.DatasetLock.state: LockState.STUCK
753
+ })
754
+ session.execute(stmt)
755
+ elif new_rule.locks_replicating_cnt == 0:
756
+ new_rule.state = RuleState.OK
757
+ if new_rule.grouping != RuleGrouping.NONE:
758
+ stmt = update(
759
+ models.DatasetLock
760
+ ).where(
761
+ models.DatasetLock.rule_id == new_rule.id
762
+ ).values({
763
+ models.DatasetLock.state: LockState.OK
764
+ })
765
+ session.execute(stmt)
766
+ session.flush()
767
+ if new_rule.notification == RuleNotification.YES:
768
+ generate_email_for_rule_ok_notification(rule=new_rule, session=session)
769
+ generate_rule_notifications(rule=new_rule, replicating_locks_before=0, session=session)
770
+ else:
771
+ new_rule.state = RuleState.REPLICATING
772
+ if new_rule.grouping != RuleGrouping.NONE:
773
+ stmt = update(
774
+ models.DatasetLock
775
+ ).where(
776
+ models.DatasetLock.rule_id == new_rule.id
777
+ ).values({
778
+ models.DatasetLock.state: LockState.REPLICATING
779
+ })
780
+ session.execute(stmt)
781
+
782
+ # Add rule to History
783
+ insert_rule_history(rule=new_rule, recent=True, longterm=True, session=session)
784
+
785
+ logger(logging.INFO, "Created rule %s [%d/%d/%d] in state %s", str(new_rule.id), new_rule.locks_ok_cnt, new_rule.locks_replicating_cnt, new_rule.locks_stuck_cnt, str(new_rule.state))
786
+
787
+ return rule_ids
788
+
789
+
790
+ @transactional_session
791
+ def inject_rule(
792
+ rule_id: str,
793
+ *,
794
+ session: "Session",
795
+ logger: LoggerFunction = logging.log
796
+ ) -> None:
797
+ """
798
+ Inject a replication rule.
799
+
800
+ :param rule_id: The id of the rule to inject.
801
+ :param new_owner: The new owner of the rule.
802
+ :param session: The database session in use.
803
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
804
+ :raises: InvalidReplicationRule, InsufficientAccountLimit, InvalidRSEExpression, DataId, RSEOverQuota
805
+ """
806
+
807
+ try:
808
+ stmt = select(
809
+ models.ReplicationRule
810
+ ).where(
811
+ models.ReplicationRule.id == rule_id
812
+ ).with_for_update(
813
+ nowait=True
814
+ )
815
+ rule = session.execute(stmt).scalar_one()
816
+ except NoResultFound as exc:
817
+ raise RuleNotFound('No rule with the id %s found' % (rule_id)) from exc
818
+
819
+ # Check if rule will expire in the next 5 minutes:
820
+ if rule.child_rule_id is None and rule.expires_at is not None and rule.expires_at < datetime.utcnow() + timedelta(seconds=300):
821
+ logger(logging.INFO, 'Rule %s expiring soon, skipping', str(rule.id))
822
+ return
823
+
824
+ # Special R2D2 container handling
825
+ if (rule.did_type == DIDType.CONTAINER and '.r2d2_request.' in rule.name) or (rule.split_container and rule.did_type == DIDType.CONTAINER):
826
+ logger(logging.DEBUG, "Creating dataset rules for Split Container rule %s", str(rule.id))
827
+ # Get all child datasets and put rules on them
828
+ dids = [{'scope': dataset['scope'], 'name': dataset['name']} for dataset in rucio.core.did.list_child_datasets(scope=rule.scope, name=rule.name, session=session)]
829
+ # Remove duplicates from the list of dictionaries
830
+ dids = [dict(t) for t in {tuple(d.items()) for d in dids}]
831
+ # Remove dids which already have a similar rule
832
+ stmt = select(
833
+ models.ReplicationRule.id
834
+ ).where(
835
+ and_(models.ReplicationRule.account == rule.account,
836
+ models.ReplicationRule.rse_expression == rule.rse_expression)
837
+ )
838
+ dids = [did for did in dids if session.execute(stmt.where(and_(models.ReplicationRule.scope == did['scope'], models.ReplicationRule.name == did['name']))).scalar_one_or_none() is None]
839
+ if rule.expires_at:
840
+ lifetime = (rule.expires_at - datetime.utcnow()).days * 24 * 3600 + (rule.expires_at - datetime.utcnow()).seconds
841
+ else:
842
+ lifetime = None
843
+
844
+ notify = {RuleNotification.YES: 'Y', RuleNotification.CLOSE: 'C', RuleNotification.PROGRESS: 'P'}.get(rule.notification, 'N')
845
+
846
+ add_rule(dids=dids,
847
+ account=rule.account,
848
+ copies=rule.copies,
849
+ rse_expression=rule.rse_expression,
850
+ grouping='DATASET',
851
+ weight=None,
852
+ lifetime=lifetime,
853
+ locked=False,
854
+ subscription_id=None,
855
+ activity=rule.activity,
856
+ notify=notify,
857
+ comment=rule.comments,
858
+ asynchronous=True,
859
+ ignore_availability=rule.ignore_availability,
860
+ ignore_account_limit=True,
861
+ priority=rule.priority,
862
+ split_container=rule.split_container,
863
+ session=session)
864
+ rule.delete(session=session)
865
+ return
866
+
867
+ # 1. Resolve the rse_expression into a list of RSE-ids
868
+ with METRICS.timer('inject_rule.parse_rse_expression'):
869
+ vo = rule['account'].vo
870
+ if rule.ignore_availability:
871
+ rses = parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session)
872
+ else:
873
+ rses = parse_expression(rule.rse_expression, filter_={'vo': vo, 'availability_write': True}, session=session)
874
+
875
+ if rule.source_replica_expression:
876
+ source_rses = parse_expression(rule.source_replica_expression, filter_={'vo': vo}, session=session)
877
+ else:
878
+ source_rses = []
879
+
880
+ # 2. Create the rse selector
881
+ with METRICS.timer('inject_rule.create_rse_selector'):
882
+ rseselector = RSESelector(account=rule['account'], rses=rses, weight=rule.weight, copies=rule.copies, ignore_account_limit=rule.ignore_account_limit, session=session)
883
+
884
+ # 3. Get the did
885
+ with METRICS.timer('inject_rule.get_did'):
886
+ try:
887
+ stmt = select(
888
+ models.DataIdentifier
889
+ ).where(
890
+ and_(models.DataIdentifier.scope == rule.scope,
891
+ models.DataIdentifier.name == rule.name)
892
+ )
893
+ did = session.execute(stmt).scalar_one()
894
+ except NoResultFound as exc:
895
+ raise DataIdentifierNotFound('Data identifier %s:%s is not valid.' % (rule.scope, rule.name)) from exc
896
+ except TypeError as error:
897
+ raise InvalidObject(error.args) from error
898
+
899
+ # 4. Apply the rule
900
+ with METRICS.timer('inject_rule.apply_rule'):
901
+ try:
902
+ apply_rule(did, rule, [x['id'] for x in rses], [x['id'] for x in source_rses], rseselector, session=session)
903
+ except IntegrityError as error:
904
+ raise ReplicationRuleCreationTemporaryFailed(error.args[0]) from error
905
+
906
+ if rule.locks_stuck_cnt > 0:
907
+ rule.state = RuleState.STUCK
908
+ rule.error = 'MissingSourceReplica'
909
+ if rule.grouping != RuleGrouping.NONE:
910
+ stmt = update(
911
+ models.DatasetLock
912
+ ).where(
913
+ models.DatasetLock.rule_id == rule.id
914
+ ).values({
915
+ models.DatasetLock.state: LockState.STUCK
916
+ })
917
+ session.execute(stmt)
918
+ elif rule.locks_replicating_cnt == 0:
919
+ rule.state = RuleState.OK
920
+ if rule.grouping != RuleGrouping.NONE:
921
+ stmt = update(
922
+ models.DatasetLock
923
+ ).where(
924
+ models.DatasetLock.rule_id == rule.id
925
+ ).values({
926
+ models.DatasetLock.state: LockState.OK
927
+ })
928
+ session.execute(stmt)
929
+ session.flush()
930
+ if rule.notification == RuleNotification.YES:
931
+ generate_email_for_rule_ok_notification(rule=rule, session=session)
932
+ generate_rule_notifications(rule=rule, replicating_locks_before=0, session=session)
933
+ # Try to release potential parent rules
934
+ release_parent_rule(child_rule_id=rule.id, session=session)
935
+ else:
936
+ rule.state = RuleState.REPLICATING
937
+ if rule.grouping != RuleGrouping.NONE:
938
+ stmt = update(
939
+ models.DatasetLock
940
+ ).where(
941
+ models.DatasetLock.rule_id == rule.id
942
+ ).values({
943
+ models.DatasetLock.state: LockState.REPLICATING
944
+ })
945
+ session.execute(stmt)
946
+
947
+ # Add rule to History
948
+ insert_rule_history(rule=rule, recent=True, longterm=True, session=session)
949
+
950
+ logger(logging.INFO, "Created rule %s [%d/%d/%d] with new algorithm for did %s:%s in state %s", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt, rule.scope, rule.name, str(rule.state))
951
+
952
+
953
+ @stream_session
954
+ def list_rules(
955
+ filters: Optional[dict[str, Any]] = None,
956
+ *,
957
+ session: "Session"
958
+ ) -> 'Iterator[dict[str, Any]]':
959
+ """
960
+ List replication rules.
961
+
962
+ :param filters: dictionary of attributes by which the results should be filtered.
963
+ :param session: The database session in use.
964
+ :raises: RucioException
965
+ """
966
+
967
+ stmt = select(
968
+ models.ReplicationRule,
969
+ models.DataIdentifier.bytes
970
+ ).join(
971
+ models.DataIdentifier,
972
+ and_(
973
+ models.ReplicationRule.scope == models.DataIdentifier.scope,
974
+ models.ReplicationRule.name == models.DataIdentifier.name
975
+ )
976
+ )
977
+ if filters is not None:
978
+ for (key, value) in filters.items():
979
+ if key in ['account', 'scope']:
980
+ if '*' in value.internal:
981
+ value = value.internal.replace('*', '%')
982
+ stmt = stmt.where(getattr(models.ReplicationRule, key).like(value))
983
+ continue
984
+ # else fall through
985
+ elif key == 'created_before':
986
+ stmt = stmt.where(models.ReplicationRule.created_at <= str_to_date(value))
987
+ continue
988
+ elif key == 'created_after':
989
+ stmt = stmt.where(models.ReplicationRule.created_at >= str_to_date(value))
990
+ continue
991
+ elif key == 'updated_before':
992
+ stmt = stmt.where(models.ReplicationRule.updated_at <= str_to_date(value))
993
+ continue
994
+ elif key == 'updated_after':
995
+ stmt = stmt.where(models.ReplicationRule.updated_at >= str_to_date(value))
996
+ continue
997
+ elif key == 'state':
998
+ if isinstance(value, str):
999
+ value = RuleState(value)
1000
+ else:
1001
+ try:
1002
+ value = RuleState[value]
1003
+ except ValueError:
1004
+ pass
1005
+ elif key == 'did_type' and isinstance(value, str):
1006
+ value = DIDType(value)
1007
+ elif key == 'grouping' and isinstance(value, str):
1008
+ value = RuleGrouping(value)
1009
+ stmt = stmt.where(getattr(models.ReplicationRule, key) == value)
1010
+
1011
+ try:
1012
+ for rule, data_identifier_bytes in session.execute(stmt).yield_per(5):
1013
+ d = rule.to_dict()
1014
+ d['bytes'] = data_identifier_bytes
1015
+ yield d
1016
+ except StatementError as exc:
1017
+ raise RucioException('Badly formatted input (IDs?)') from exc
1018
+
1019
+
1020
+ @stream_session
1021
+ def list_rule_history(
1022
+ rule_id: str,
1023
+ *,
1024
+ session: "Session"
1025
+ ) -> 'Iterator[dict[str, Any]]':
1026
+ """
1027
+ List the rule history of a rule.
1028
+
1029
+ :param rule_id: The id of the rule.
1030
+ :param session: The database session in use.
1031
+ :raises: RucioException
1032
+ """
1033
+
1034
+ stmt = select(
1035
+ models.ReplicationRuleHistory.updated_at,
1036
+ models.ReplicationRuleHistory.state,
1037
+ models.ReplicationRuleHistory.locks_ok_cnt,
1038
+ models.ReplicationRuleHistory.locks_stuck_cnt,
1039
+ models.ReplicationRuleHistory.locks_replicating_cnt
1040
+ ).where(
1041
+ models.ReplicationRuleHistory.id == rule_id
1042
+ ).order_by(
1043
+ models.ReplicationRuleHistory.updated_at
1044
+ )
1045
+
1046
+ try:
1047
+ for rule in session.execute(stmt).yield_per(5):
1048
+ yield rule._asdict()
1049
+ except StatementError as exc:
1050
+ raise RucioException('Badly formatted input (IDs?)') from exc
1051
+
1052
+
1053
+ @stream_session
1054
+ def list_rule_full_history(
1055
+ scope: InternalScope,
1056
+ name: str,
1057
+ *,
1058
+ session: "Session"
1059
+ ) -> 'Iterator[dict[str, Any]]':
1060
+ """
1061
+ List the rule history of a DID.
1062
+
1063
+ :param scope: The scope of the DID.
1064
+ :param name: The name of the DID.
1065
+ :param session: The database session in use.
1066
+ :raises: RucioException
1067
+ """
1068
+
1069
+ stmt = select(
1070
+ models.ReplicationRuleHistory.id.label('rule_id'),
1071
+ models.ReplicationRuleHistory.created_at,
1072
+ models.ReplicationRuleHistory.updated_at,
1073
+ models.ReplicationRuleHistory.rse_expression,
1074
+ models.ReplicationRuleHistory.state,
1075
+ models.ReplicationRuleHistory.account,
1076
+ models.ReplicationRuleHistory.locks_ok_cnt,
1077
+ models.ReplicationRuleHistory.locks_stuck_cnt,
1078
+ models.ReplicationRuleHistory.locks_replicating_cnt
1079
+ ).with_hint(
1080
+ models.ReplicationRuleHistory, 'INDEX(RULES_HISTORY_SCOPENAME_IDX)', 'oracle'
1081
+ ).where(
1082
+ and_(models.ReplicationRuleHistory.scope == scope,
1083
+ models.ReplicationRuleHistory.name == name)
1084
+ ).order_by(
1085
+ models.ReplicationRuleHistory.created_at,
1086
+ models.ReplicationRuleHistory.updated_at
1087
+ )
1088
+ for rule in session.execute(stmt).yield_per(5):
1089
+ yield rule._asdict()
1090
+
1091
+
1092
+ @stream_session
1093
+ def list_associated_rules_for_file(
1094
+ scope: InternalScope,
1095
+ name: str,
1096
+ *,
1097
+ session: "Session"
1098
+ ) -> 'Iterator[dict[str, Any]]':
1099
+ """
1100
+ List replication rules a file is affected from.
1101
+
1102
+ :param scope: Scope of the file.
1103
+ :param name: Name of the file.
1104
+ :param session: The database session in use.
1105
+ :raises: RucioException
1106
+ """
1107
+ rucio.core.did.get_did(scope=scope, name=name, session=session) # Check if the did actually exists
1108
+ stmt = select(
1109
+ models.ReplicationRule,
1110
+ models.DataIdentifier.bytes
1111
+ ).distinct(
1112
+ ).join(
1113
+ models.ReplicaLock,
1114
+ models.ReplicationRule.id == models.ReplicaLock.rule_id
1115
+ ).join(
1116
+ models.DataIdentifier,
1117
+ and_(models.ReplicationRule.scope == models.DataIdentifier.scope,
1118
+ models.ReplicationRule.name == models.DataIdentifier.name)
1119
+ ).with_hint(
1120
+ models.ReplicaLock, 'INDEX(LOCKS LOCKS_PK)', 'oracle'
1121
+ ).where(
1122
+ and_(models.ReplicaLock.scope == scope,
1123
+ models.ReplicaLock.name == name)
1124
+ )
1125
+ try:
1126
+ for rule, data_identifier_bytes in session.execute(stmt).yield_per(5):
1127
+ d = rule.to_dict()
1128
+ d['bytes'] = data_identifier_bytes
1129
+ yield d
1130
+ except StatementError as exc:
1131
+ raise RucioException('Badly formatted input (IDs?)') from exc
1132
+
1133
+
1134
+ @transactional_session
1135
+ def delete_rule(
1136
+ rule_id: str,
1137
+ purge_replicas: Optional[bool] = None,
1138
+ soft: bool = False,
1139
+ delete_parent: bool = False,
1140
+ nowait: bool = False,
1141
+ *,
1142
+ session: "Session",
1143
+ ignore_rule_lock: bool = False
1144
+ ) -> None:
1145
+ """
1146
+ Delete a replication rule.
1147
+
1148
+ :param rule_id: The rule to delete.
1149
+ :param purge_replicas: Purge the replicas immediately.
1150
+ :param soft: Only perform a soft deletion.
1151
+ :param delete_parent: Delete rules even if they have a child_rule_id set.
1152
+ :param nowait: Nowait parameter for the FOR UPDATE statement.
1153
+ :param session: The database session in use.
1154
+ :param ignore_rule_lock: Ignore any locks on the rule
1155
+ :raises: RuleNotFound if no Rule can be found.
1156
+ :raises: UnsupportedOperation if the Rule is locked.
1157
+ """
1158
+
1159
+ with METRICS.timer('delete_rule.total'):
1160
+ try:
1161
+ stmt = select(
1162
+ models.ReplicationRule
1163
+ ).where(
1164
+ models.ReplicationRule.id == rule_id
1165
+ ).with_for_update(
1166
+ nowait=nowait
1167
+ )
1168
+ rule = session.execute(stmt).scalar_one()
1169
+ except NoResultFound as exc:
1170
+ raise RuleNotFound('No rule with the id %s found' % rule_id) from exc
1171
+ if rule.locked and not ignore_rule_lock:
1172
+ raise UnsupportedOperation('The replication rule is locked and has to be unlocked before it can be deleted.')
1173
+
1174
+ if rule.child_rule_id is not None and not delete_parent:
1175
+ raise UnsupportedOperation('The replication rule has a child rule and thus cannot be deleted.')
1176
+
1177
+ if purge_replicas is not None:
1178
+ rule.purge_replicas = purge_replicas
1179
+
1180
+ if soft:
1181
+ if rule.expires_at:
1182
+ rule.expires_at = min(datetime.utcnow() + timedelta(seconds=3600), rule.expires_at)
1183
+ else:
1184
+ rule.expires_at = datetime.utcnow() + timedelta(seconds=3600)
1185
+ if rule.child_rule_id is not None and delete_parent:
1186
+ rule.child_rule_id = None
1187
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
1188
+ return
1189
+
1190
+ stmt = select(
1191
+ models.ReplicaLock
1192
+ ).where(
1193
+ models.ReplicaLock.rule_id == rule_id
1194
+ ).with_for_update(
1195
+ nowait=nowait
1196
+ )
1197
+ results = session.execute(stmt).yield_per(100)
1198
+
1199
+ # Remove locks, set tombstone if applicable
1200
+ transfers_to_delete = [] # [{'scope': , 'name':, 'rse_id':}]
1201
+ account_counter_decreases = {} # {'rse_id': [file_size, file_size, file_size]}
1202
+
1203
+ for result in results:
1204
+ lock = result[0]
1205
+ if __delete_lock_and_update_replica(lock=lock, purge_replicas=rule.purge_replicas,
1206
+ nowait=nowait, session=session):
1207
+ transfers_to_delete.append({'scope': lock.scope, 'name': lock.name, 'rse_id': lock.rse_id})
1208
+ if lock.rse_id not in account_counter_decreases:
1209
+ account_counter_decreases[lock.rse_id] = []
1210
+ account_counter_decreases[lock.rse_id].append(lock.bytes)
1211
+
1212
+ # Delete the DatasetLocks
1213
+ stmt = delete(
1214
+ models.DatasetLock
1215
+ ).where(
1216
+ models.DatasetLock.rule_id == rule_id
1217
+ ).execution_options(
1218
+ synchronize_session=False
1219
+ )
1220
+ session.execute(stmt)
1221
+
1222
+ # Decrease account_counters
1223
+ for rse_id in account_counter_decreases.keys():
1224
+ account_counter.decrease(rse_id=rse_id, account=rule.account, files=len(account_counter_decreases[rse_id]),
1225
+ bytes_=sum(account_counter_decreases[rse_id]), session=session)
1226
+
1227
+ # Try to release potential parent rules
1228
+ release_parent_rule(child_rule_id=rule.id, remove_parent_expiration=True, session=session)
1229
+
1230
+ # Insert history
1231
+ insert_rule_history(rule=rule, recent=False, longterm=True, session=session)
1232
+
1233
+ session.flush()
1234
+ rule.delete(session=session)
1235
+
1236
+ for transfer in transfers_to_delete:
1237
+ transfers_to_cancel = request_core.cancel_request_did(scope=transfer['scope'], name=transfer['name'],
1238
+ dest_rse_id=transfer['rse_id'], session=session)
1239
+ transfer_core.cancel_transfers(transfers_to_cancel)
1240
+
1241
+
1242
+ @transactional_session
1243
+ def repair_rule(
1244
+ rule_id: str,
1245
+ *,
1246
+ session: "Session",
1247
+ logger: LoggerFunction = logging.log
1248
+ ) -> None:
1249
+ """
1250
+ Repair a STUCK replication rule.
1251
+
1252
+ :param rule_id: The rule to repair.
1253
+ :param session: The database session in use.
1254
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
1255
+ """
1256
+
1257
+ # Rule error cases:
1258
+ # (A) A rule gets an exception on rule-creation. This can only be the MissingSourceReplica exception.
1259
+ # (B) A rule gets an error when re-evaluated: InvalidRSEExpression, InvalidRuleWeight, InsufficientTargetRSEs, RSEWriteBlocked
1260
+ # InsufficientAccountLimit. The re-evaluation has to be done again and potential missing locks have to be
1261
+ # created.
1262
+ # (C) Transfers fail and mark locks (and the rule) as STUCK. All STUCK locks have to be repaired.
1263
+ # (D) Files are declared as BAD.
1264
+
1265
+ # start_time = time.time()
1266
+ try:
1267
+ stmt = select(
1268
+ models.ReplicationRule
1269
+ ).where(
1270
+ models.ReplicationRule.id == rule_id
1271
+ ).with_for_update(
1272
+ nowait=True
1273
+ )
1274
+ rule = session.execute(stmt).scalar_one()
1275
+ rule.updated_at = datetime.utcnow()
1276
+
1277
+ # Check if rule is longer than 2 weeks in STUCK
1278
+ if rule.stuck_at is None:
1279
+ rule.stuck_at = datetime.utcnow()
1280
+ if rule.stuck_at < (datetime.utcnow() - timedelta(days=14)):
1281
+ rule.state = RuleState.SUSPENDED
1282
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
1283
+ logger(logging.INFO, 'Replication rule %s has been SUSPENDED', rule_id)
1284
+ return
1285
+
1286
+ # Evaluate the RSE expression to see if there is an alternative RSE anyway
1287
+ try:
1288
+ vo = rule.account.vo
1289
+ rses = parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session)
1290
+ if rule.ignore_availability:
1291
+ target_rses = parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session)
1292
+ else:
1293
+ target_rses = parse_expression(rule.rse_expression, filter_={'vo': vo, 'availability_write': True}, session=session)
1294
+ if rule.source_replica_expression:
1295
+ source_rses = parse_expression(rule.source_replica_expression, filter_={'vo': vo}, session=session)
1296
+ else:
1297
+ source_rses = []
1298
+ except (InvalidRSEExpression, RSEWriteBlocked) as error:
1299
+ rule.state = RuleState.STUCK
1300
+ rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
1301
+ rule.save(session=session)
1302
+ # Insert rule history
1303
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
1304
+ # Try to update the DatasetLocks
1305
+ if rule.grouping != RuleGrouping.NONE:
1306
+ stmt = update(
1307
+ models.DatasetLock
1308
+ ).where(
1309
+ models.DatasetLock.rule_id == rule.id
1310
+ ).values({
1311
+ models.DatasetLock.state: LockState.STUCK
1312
+ })
1313
+ session.execute(stmt)
1314
+ logger(logging.DEBUG, '%s while repairing rule %s', str(error), rule_id)
1315
+ return
1316
+
1317
+ # Create the RSESelector
1318
+ try:
1319
+ rseselector = RSESelector(account=rule.account,
1320
+ rses=target_rses,
1321
+ weight=rule.weight,
1322
+ copies=rule.copies,
1323
+ ignore_account_limit=rule.ignore_account_limit,
1324
+ session=session)
1325
+ except (InvalidRuleWeight, InsufficientTargetRSEs, InsufficientAccountLimit) as error:
1326
+ rule.state = RuleState.STUCK
1327
+ rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
1328
+ rule.save(session=session)
1329
+ # Insert rule history
1330
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
1331
+ # Try to update the DatasetLocks
1332
+ if rule.grouping != RuleGrouping.NONE:
1333
+ stmt = update(
1334
+ models.DatasetLock
1335
+ ).where(
1336
+ models.DatasetLock.rule_id == rule.id
1337
+ ).values({
1338
+ models.DatasetLock.state: LockState.STUCK
1339
+ })
1340
+ session.execute(stmt)
1341
+ logger(logging.DEBUG, '%s while repairing rule %s', type(error).__name__, rule_id)
1342
+ return
1343
+
1344
+ # Reset the counters
1345
+ logger(logging.DEBUG, "Resetting counters for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
1346
+ rule.locks_ok_cnt = 0
1347
+ rule.locks_replicating_cnt = 0
1348
+ rule.locks_stuck_cnt = 0
1349
+ stmt = select(
1350
+ models.ReplicaLock.state,
1351
+ func.count(models.ReplicaLock.state).label('state_counter')
1352
+ ).where(
1353
+ models.ReplicaLock.rule_id == rule.id
1354
+ ).group_by(
1355
+ models.ReplicaLock.state
1356
+ )
1357
+ rule_counts = session.execute(stmt).all()
1358
+ for count in rule_counts:
1359
+ if count.state == LockState.OK:
1360
+ rule.locks_ok_cnt = count.state_counter
1361
+ elif count.state == LockState.REPLICATING:
1362
+ rule.locks_replicating_cnt = count.state_counter
1363
+ elif count.state == LockState.STUCK:
1364
+ rule.locks_stuck_cnt = count.state_counter
1365
+ logger(logging.DEBUG, "Finished resetting counters for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
1366
+
1367
+ # Get the did
1368
+ stmt = select(
1369
+ models.DataIdentifier
1370
+ ).where(
1371
+ and_(models.DataIdentifier.scope == rule.scope,
1372
+ models.DataIdentifier.name == rule.name)
1373
+ )
1374
+ did = session.execute(stmt).scalar_one()
1375
+
1376
+ # Detect if there is something wrong with the dataset and
1377
+ # make the decisison on soft or hard repair.
1378
+ hard_repair = False
1379
+ if did.did_type != DIDType.FILE:
1380
+ nr_files = rucio.core.did.get_did(scope=rule.scope, name=rule.name, dynamic_depth=DIDType.FILE, session=session)['length']
1381
+ else:
1382
+ nr_files = 1
1383
+ if nr_files * rule.copies != (rule.locks_ok_cnt + rule.locks_stuck_cnt + rule.locks_replicating_cnt):
1384
+ hard_repair = True
1385
+ logger(logging.DEBUG, 'Repairing rule %s in HARD mode', str(rule.id))
1386
+ elif rule.copies > 1 and rule.grouping == RuleGrouping.NONE:
1387
+ hard_repair = True
1388
+ logger(logging.DEBUG, 'Repairing rule %s in HARD mode', str(rule.id))
1389
+
1390
+ # Resolve the did to its contents
1391
+ datasetfiles, locks, replicas, source_replicas = __resolve_did_to_locks_and_replicas(did=did,
1392
+ nowait=True,
1393
+ restrict_rses=[rse['id'] for rse in rses],
1394
+ source_rses=[rse['id'] for rse in source_rses],
1395
+ only_stuck=not hard_repair,
1396
+ session=session)
1397
+
1398
+ session.flush()
1399
+
1400
+ # 1. Try to find missing locks and create them based on grouping
1401
+ if did.did_type != DIDType.FILE and hard_repair:
1402
+ try:
1403
+ __find_missing_locks_and_create_them(datasetfiles=datasetfiles,
1404
+ locks=locks,
1405
+ replicas=replicas,
1406
+ source_replicas=source_replicas,
1407
+ rseselector=rseselector,
1408
+ rule=rule,
1409
+ source_rses=[rse['id'] for rse in source_rses],
1410
+ session=session)
1411
+ except (InsufficientAccountLimit, InsufficientTargetRSEs) as error:
1412
+ rule.state = RuleState.STUCK
1413
+ rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
1414
+ rule.save(session=session)
1415
+ # Insert rule history
1416
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
1417
+ # Try to update the DatasetLocks
1418
+ if rule.grouping != RuleGrouping.NONE:
1419
+ stmt = update(
1420
+ models.DatasetLock
1421
+ ).where(
1422
+ models.DatasetLock.rule_id == rule.id
1423
+ ).values({
1424
+ models.DatasetLock.state: LockState.STUCK
1425
+ })
1426
+ session.execute(stmt)
1427
+ logger(logging.DEBUG, '%s while repairing rule %s', type(error).__name__, rule_id)
1428
+ return
1429
+
1430
+ session.flush()
1431
+
1432
+ # 2. Try to find surplus locks and remove them
1433
+ if hard_repair:
1434
+ __find_surplus_locks_and_remove_them(datasetfiles=datasetfiles,
1435
+ locks=locks,
1436
+ rule=rule,
1437
+ session=session)
1438
+
1439
+ session.flush()
1440
+
1441
+ # 3. Try to find STUCK locks and repair them based on grouping
1442
+ try:
1443
+ __find_stuck_locks_and_repair_them(datasetfiles=datasetfiles,
1444
+ locks=locks,
1445
+ replicas=replicas,
1446
+ source_replicas=source_replicas,
1447
+ rseselector=rseselector,
1448
+ rule=rule,
1449
+ source_rses=[rse['id'] for rse in source_rses],
1450
+ session=session)
1451
+ except (InsufficientAccountLimit, InsufficientTargetRSEs) as error:
1452
+ rule.state = RuleState.STUCK
1453
+ rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
1454
+ rule.save(session=session)
1455
+ # Insert rule history
1456
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
1457
+ # Try to update the DatasetLocks
1458
+ if rule.grouping != RuleGrouping.NONE:
1459
+ stmt = update(
1460
+ models.DatasetLock
1461
+ ).where(
1462
+ models.DatasetLock.rule_id == rule.id
1463
+ ).values({
1464
+ models.DatasetLock.state: LockState.STUCK
1465
+ })
1466
+ session.execute(stmt)
1467
+ logger(logging.DEBUG, '%s while repairing rule %s', type(error).__name__, rule_id)
1468
+ return
1469
+
1470
+ # Delete Datasetlocks which are not relevant anymore
1471
+ stmt = select(
1472
+ models.ReplicaLock.rse_id
1473
+ ).distinct(
1474
+ ).where(
1475
+ models.ReplicaLock.rule_id == rule.id
1476
+ )
1477
+ validated_datasetlock_rse_ids = session.execute(stmt).scalars().all()
1478
+
1479
+ stmt = select(
1480
+ models.DatasetLock
1481
+ ).where(
1482
+ models.DatasetLock.rule_id == rule.id
1483
+ )
1484
+ dataset_locks = session.execute(stmt).scalars().all()
1485
+ for dataset_lock in dataset_locks:
1486
+ if dataset_lock.rse_id not in validated_datasetlock_rse_ids:
1487
+ dataset_lock.delete(session=session)
1488
+
1489
+ if rule.locks_stuck_cnt != 0:
1490
+ logger(logging.INFO, 'Rule %s [%d/%d/%d] state=STUCK', str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
1491
+ rule.state = RuleState.STUCK
1492
+ # Insert rule history
1493
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
1494
+ # Try to update the DatasetLocks
1495
+ if rule.grouping != RuleGrouping.NONE:
1496
+ stmt = update(
1497
+ models.DatasetLock
1498
+ ).where(
1499
+ models.DatasetLock.rule_id == rule.id
1500
+ ).values({
1501
+ models.DatasetLock.state: LockState.STUCK
1502
+ })
1503
+ session.execute(stmt)
1504
+ # TODO: Increase some kind of Stuck Counter here, The rule should at some point be SUSPENDED
1505
+ return
1506
+
1507
+ rule.stuck_at = None
1508
+
1509
+ if rule.locks_replicating_cnt > 0:
1510
+ logger(logging.INFO, 'Rule %s [%d/%d/%d] state=REPLICATING', str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
1511
+ rule.state = RuleState.REPLICATING
1512
+ rule.error = None
1513
+ # Insert rule history
1514
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
1515
+ # Try to update the DatasetLocks
1516
+ if rule.grouping != RuleGrouping.NONE:
1517
+ stmt = update(
1518
+ models.DatasetLock
1519
+ ).where(
1520
+ models.DatasetLock.rule_id == rule.id
1521
+ ).values({
1522
+ models.DatasetLock.state: LockState.REPLICATING
1523
+ })
1524
+ session.execute(stmt)
1525
+ return
1526
+
1527
+ rule.state = RuleState.OK
1528
+ rule.error = None
1529
+ # Insert rule history
1530
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
1531
+ logger(logging.INFO, 'Rule %s [%d/%d/%d] state=OK', str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
1532
+
1533
+ if rule.grouping != RuleGrouping.NONE:
1534
+ stmt = update(
1535
+ models.DatasetLock
1536
+ ).where(
1537
+ models.DatasetLock.rule_id == rule.id
1538
+ ).values({
1539
+ models.DatasetLock.state: LockState.OK
1540
+ })
1541
+ session.execute(stmt)
1542
+ session.flush()
1543
+ if rule.notification == RuleNotification.YES:
1544
+ generate_email_for_rule_ok_notification(rule=rule, session=session)
1545
+ generate_rule_notifications(rule=rule, replicating_locks_before=0, session=session)
1546
+ # Try to release potential parent rules
1547
+ rucio.core.rule.release_parent_rule(child_rule_id=rule.id, session=session)
1548
+
1549
+ return
1550
+
1551
+ except NoResultFound:
1552
+ # The rule has been deleted in the meanwhile
1553
+ return
1554
+
1555
+
1556
+ @read_session
1557
+ def get_rule(
1558
+ rule_id: str,
1559
+ *,
1560
+ session: "Session"
1561
+ ) -> dict[str, Any]:
1562
+ """
1563
+ Get a specific replication rule.
1564
+
1565
+ :param rule_id: The rule_id to select.
1566
+ :param session: The database session in use.
1567
+ :raises: RuleNotFound if no Rule can be found.
1568
+ """
1569
+
1570
+ try:
1571
+ stmt = select(
1572
+ models.ReplicationRule
1573
+ ).where(
1574
+ models.ReplicationRule.id == rule_id
1575
+ )
1576
+ rule = session.execute(stmt).scalar_one()
1577
+ return rule.to_dict()
1578
+ except NoResultFound as exc:
1579
+ raise RuleNotFound('No rule with the id %s found' % (rule_id)) from exc
1580
+ except StatementError as exc:
1581
+ raise RucioException('Badly formatted rule id (%s)' % (rule_id)) from exc
1582
+
1583
+
1584
+ @transactional_session
1585
+ def update_rule(
1586
+ rule_id: str,
1587
+ options: dict[str, Any],
1588
+ *,
1589
+ session: "Session"
1590
+ ) -> None:
1591
+ """
1592
+ Update a rules options.
1593
+
1594
+ :param rule_id: The rule_id to lock.
1595
+ :param options: Dictionary of options
1596
+ :param session: The database session in use.
1597
+ :raises: RuleNotFound if no Rule can be found, InputValidationError if invalid option is used, ScratchDiskLifetimeConflict if wrong ScratchDiskLifetime is used.
1598
+ """
1599
+
1600
+ valid_options = ['comment', 'locked', 'lifetime', 'account', 'state',
1601
+ 'activity', 'source_replica_expression', 'cancel_requests',
1602
+ 'priority', 'child_rule_id', 'eol_at', 'meta',
1603
+ 'purge_replicas', 'boost_rule']
1604
+
1605
+ for key in options:
1606
+ if key not in valid_options:
1607
+ raise InputValidationError('%s is not a valid option to set.' % key)
1608
+
1609
+ try:
1610
+ query = select(
1611
+ models.ReplicationRule
1612
+ ).where(
1613
+ models.ReplicationRule.id == rule_id
1614
+ )
1615
+ rule: models.ReplicationRule = session.execute(query).scalar_one()
1616
+
1617
+ for key in options:
1618
+ if key == 'lifetime':
1619
+ # Check SCRATCHDISK Policy
1620
+ vo = rule.account.vo
1621
+ rses = parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session)
1622
+ try:
1623
+ lifetime = get_scratch_policy(rule.account, rses, options['lifetime'], session=session)
1624
+ except UndefinedPolicy:
1625
+ lifetime = options['lifetime']
1626
+ rule.expires_at = datetime.utcnow() + timedelta(seconds=lifetime) if lifetime is not None else None
1627
+ if key == 'source_replica_expression':
1628
+ rule.source_replica_expression = options['source_replica_expression']
1629
+
1630
+ if key == 'comment':
1631
+ rule.comments = options['comment']
1632
+
1633
+ if key == 'activity':
1634
+ validate_schema(
1635
+ 'activity', options['activity'], vo=rule.account.vo
1636
+ )
1637
+ rule.activity = options['activity']
1638
+ # Cancel transfers and re-submit them:
1639
+ query = select(
1640
+ models.ReplicaLock
1641
+ ).where(
1642
+ models.ReplicaLock.rule_id == rule.id,
1643
+ models.ReplicaLock.state == LockState.REPLICATING
1644
+ )
1645
+ for lock in session.execute(query).scalars().all():
1646
+ transfers_to_cancel = request_core.cancel_request_did(
1647
+ scope=lock.scope,
1648
+ name=lock.name,
1649
+ dest_rse_id=lock.rse_id,
1650
+ session=session
1651
+ )
1652
+ transfer_core.cancel_transfers(transfers_to_cancel)
1653
+ query = select(
1654
+ models.RSEFileAssociation.md5,
1655
+ models.RSEFileAssociation.bytes,
1656
+ models.RSEFileAssociation.adler32
1657
+ ).where(
1658
+ models.RSEFileAssociation.scope == lock.scope,
1659
+ models.RSEFileAssociation.name == lock.name,
1660
+ models.RSEFileAssociation.rse_id == lock.rse_id
1661
+ )
1662
+ md5, bytes_, adler32 = session.execute(query).one()
1663
+ session.flush()
1664
+
1665
+ requests = create_transfer_dict(
1666
+ dest_rse_id=lock.rse_id,
1667
+ request_type=RequestType.TRANSFER,
1668
+ scope=lock.scope,
1669
+ name=lock.name,
1670
+ rule=rule,
1671
+ lock=lock,
1672
+ bytes_=bytes_,
1673
+ md5=md5,
1674
+ adler32=adler32,
1675
+ ds_scope=rule.scope,
1676
+ ds_name=rule.name,
1677
+ copy_pin_lifetime=None,
1678
+ activity=rule.activity,
1679
+ session=session
1680
+ )
1681
+ request_core.queue_requests([requests], session=session)
1682
+
1683
+ elif key == 'account':
1684
+ # Check if the account exists
1685
+ get_account(options['account'], session=session)
1686
+ # Update locks
1687
+ query = select(
1688
+ models.ReplicaLock
1689
+ ).where(
1690
+ models.ReplicaLock.rule_id == rule.id
1691
+ )
1692
+ counter_rses = {}
1693
+ for lock in session.execute(query).scalars().all():
1694
+ if lock.rse_id in counter_rses:
1695
+ counter_rses[lock.rse_id].append(lock.bytes)
1696
+ else:
1697
+ counter_rses[lock.rse_id] = [lock.bytes]
1698
+ for locktype in (models.ReplicaLock, models.DatasetLock):
1699
+ query = update(
1700
+ locktype
1701
+ ).where(
1702
+ locktype.rule_id == rule.id
1703
+ ).values({
1704
+ locktype.account: options['account']
1705
+ })
1706
+ session.execute(query)
1707
+
1708
+ # Update counters
1709
+ for rse_id in counter_rses:
1710
+ account_counter.decrease(
1711
+ rse_id=rse_id,
1712
+ account=rule.account,
1713
+ files=len(counter_rses[rse_id]),
1714
+ bytes_=sum(counter_rses[rse_id]),
1715
+ session=session
1716
+ )
1717
+ account_counter.increase(
1718
+ rse_id=rse_id,
1719
+ account=options['account'],
1720
+ files=len(counter_rses[rse_id]),
1721
+ bytes_=sum(counter_rses[rse_id]),
1722
+ session=session
1723
+ )
1724
+ # Update rule
1725
+ rule.account = options['account']
1726
+ session.flush()
1727
+
1728
+ elif key == 'state':
1729
+ if options.get('cancel_requests', False):
1730
+ rule_ids_to_stuck = set()
1731
+ query = select(
1732
+ models.ReplicaLock
1733
+ ).where(
1734
+ models.ReplicaLock.rule_id == rule.id,
1735
+ models.ReplicaLock.state == LockState.REPLICATING
1736
+ )
1737
+ for lock in session.execute(query).scalars().all():
1738
+ # Set locks to stuck:
1739
+ query = select(
1740
+ models.ReplicaLock
1741
+ ).where(
1742
+ models.ReplicaLock.scope == lock.scope,
1743
+ models.ReplicaLock.name == lock.name,
1744
+ models.ReplicaLock.rse_id == lock.rse_id,
1745
+ models.ReplicaLock.state == LockState.REPLICATING
1746
+ )
1747
+ for lock2 in session.execute(query).scalars().all():
1748
+ lock2.state = LockState.STUCK
1749
+ rule_ids_to_stuck.add(lock2.rule_id)
1750
+ transfers_to_cancel = request_core.cancel_request_did(
1751
+ scope=lock.scope,
1752
+ name=lock.name,
1753
+ dest_rse_id=lock.rse_id,
1754
+ session=session
1755
+ )
1756
+ transfer_core.cancel_transfers(transfers_to_cancel)
1757
+ query = select(
1758
+ models.RSEFileAssociation
1759
+ ).where(
1760
+ models.RSEFileAssociation.scope == lock.scope,
1761
+ models.RSEFileAssociation.name == lock.name,
1762
+ models.RSEFileAssociation.rse_id == lock.rse_id
1763
+ )
1764
+ replica = session.execute(query).scalar_one()
1765
+ replica.state = ReplicaState.UNAVAILABLE
1766
+ # Set rules and DATASETLOCKS to STUCK:
1767
+ for rid in rule_ids_to_stuck:
1768
+ query = update(
1769
+ models.ReplicationRule
1770
+ ).where(
1771
+ models.ReplicationRule.id == rid,
1772
+ models.ReplicationRule.state != RuleState.SUSPENDED
1773
+ ).values({
1774
+ models.ReplicationRule.state: RuleState.STUCK
1775
+ })
1776
+ session.execute(query)
1777
+
1778
+ query = update(
1779
+ models.DatasetLock
1780
+ ).where(
1781
+ models.DatasetLock.rule_id == rid
1782
+ ).values({
1783
+ models.DatasetLock.state: LockState.STUCK
1784
+ })
1785
+ session.execute(query)
1786
+
1787
+ if options['state'].lower() == 'suspended':
1788
+ rule.state = RuleState.SUSPENDED
1789
+
1790
+ elif options['state'].lower() == 'stuck':
1791
+ rule.state = RuleState.STUCK
1792
+ rule.stuck_at = datetime.utcnow()
1793
+ if not options.get('cancel_requests', False):
1794
+ query = update(
1795
+ models.ReplicaLock
1796
+ ).where(
1797
+ models.ReplicaLock.rule_id == rule.id,
1798
+ models.ReplicaLock.state == LockState.REPLICATING
1799
+ ).values({
1800
+ models.ReplicaLock.state: LockState.STUCK
1801
+ })
1802
+ session.execute(query)
1803
+
1804
+ query = update(
1805
+ models.DatasetLock
1806
+ ).where(
1807
+ models.DatasetLock.rule_id == rule_id
1808
+ ).values({
1809
+ models.DatasetLock.state: LockState.STUCK
1810
+ })
1811
+ session.execute(query)
1812
+
1813
+ elif key == 'cancel_requests':
1814
+ pass
1815
+
1816
+ elif key == 'priority':
1817
+ try:
1818
+ rule.priority = options[key]
1819
+ transfers_to_update = request_core.update_requests_priority(priority=options[key], filter_={'rule_id': rule_id}, session=session)
1820
+ transfer_core.update_transfer_priority(transfers_to_update)
1821
+ except Exception as exc:
1822
+ raise UnsupportedOperation('The FTS Requests are already in a final state.') from exc
1823
+
1824
+ elif key == 'child_rule_id':
1825
+ # Check if the child rule has the same scope/name as the parent rule
1826
+ child_id: Optional[str] = options[key]
1827
+ if child_id is None:
1828
+ if not rule.child_rule_id:
1829
+ raise InputValidationError('Cannot detach child when no such relationship exists')
1830
+ # dissolve relationship
1831
+ rule.child_rule_id = None # type: ignore
1832
+ # remove expiration date
1833
+ rule.expires_at = None # type: ignore
1834
+ else:
1835
+ query = select(
1836
+ models.ReplicationRule
1837
+ ).where(
1838
+ models.ReplicationRule.id == child_id
1839
+ )
1840
+ child_rule = session.execute(query).scalar_one()
1841
+ if rule.scope != child_rule.scope or rule.name != child_rule.name:
1842
+ raise InputValidationError('Parent and child rule must be set on the same dataset.')
1843
+ if rule.id == options[key]:
1844
+ raise InputValidationError('Self-referencing parent/child-relationship.')
1845
+ if child_rule.state != RuleState.OK:
1846
+ rule.child_rule_id = child_id # type: ignore
1847
+
1848
+ elif key == 'meta':
1849
+ # Need to json.dump the metadata
1850
+ rule.meta = json.dumps(options[key])
1851
+
1852
+ else:
1853
+ setattr(rule, key, options[key])
1854
+
1855
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
1856
+
1857
+ # `boost_rule` should run after `stuck`, so lets not include it in the loop since the arguments are unordered
1858
+ if 'boost_rule' in options:
1859
+ query = select(
1860
+ models.ReplicaLock
1861
+ ).where(
1862
+ models.ReplicaLock.rule_id == rule.id,
1863
+ models.ReplicaLock.state == LockState.STUCK
1864
+ )
1865
+ for lock in session.execute(query).scalars().all():
1866
+ lock['updated_at'] -= timedelta(days=1)
1867
+
1868
+ rule['updated_at'] -= timedelta(days=1)
1869
+
1870
+ insert_rule_history(
1871
+ rule,
1872
+ recent=True,
1873
+ longterm=False,
1874
+ session=session
1875
+ )
1876
+
1877
+ except IntegrityError as error:
1878
+ if match('.*ORA-00001.*', str(error.args[0])) \
1879
+ or match('.*IntegrityError.*UNIQUE constraint failed.*', str(error.args[0])) \
1880
+ or match('.*1062.*Duplicate entry.*for key.*', str(error.args[0])) \
1881
+ or match('.*IntegrityError.*columns? .*not unique.*', str(error.args[0])):
1882
+ raise DuplicateRule(error.args[0]) from error
1883
+ else:
1884
+ raise error
1885
+ except NoResultFound as exc:
1886
+ raise RuleNotFound('No rule with the id %s found' % (rule_id)) from exc
1887
+ except StatementError as exc:
1888
+ raise RucioException(f"A StatementError occurred while processing rule {rule_id}") from exc
1889
+
1890
+
1891
+ @transactional_session
1892
+ def reduce_rule(
1893
+ rule_id: str,
1894
+ copies: int,
1895
+ exclude_expression: Optional[str] = None,
1896
+ *,
1897
+ session: "Session"
1898
+ ) -> str:
1899
+ """
1900
+ Reduce the number of copies for a rule by atomically replacing the rule.
1901
+
1902
+ :param rule_id: Rule to be reduced.
1903
+ :param copies: Number of copies of the new rule.
1904
+ :param exclude_expression: RSE Expression of RSEs to exclude.
1905
+ :param session: The DB Session.
1906
+ :raises: RuleReplaceFailed, RuleNotFound
1907
+ """
1908
+ try:
1909
+ stmt = select(
1910
+ models.ReplicationRule
1911
+ ).where(
1912
+ models.ReplicationRule.id == rule_id
1913
+ )
1914
+ rule = session.execute(stmt).scalar_one()
1915
+
1916
+ if copies >= rule.copies:
1917
+ raise RuleReplaceFailed('Copies of the new rule must be smaller than the old rule.')
1918
+
1919
+ if rule.state != RuleState.OK:
1920
+ raise RuleReplaceFailed('The source rule must be in state OK.')
1921
+
1922
+ if exclude_expression:
1923
+ rse_expression = '(' + rule.rse_expression + ')' + '\\' + '(' + exclude_expression + ')'
1924
+ else:
1925
+ rse_expression = rule.rse_expression
1926
+
1927
+ grouping = {RuleGrouping.ALL: 'ALL', RuleGrouping.NONE: 'NONE'}.get(rule.grouping, 'DATASET')
1928
+
1929
+ if rule.expires_at:
1930
+ lifetime = (rule.expires_at - datetime.utcnow()).days * 24 * 3600 + (rule.expires_at - datetime.utcnow()).seconds
1931
+ else:
1932
+ lifetime = None
1933
+
1934
+ notify = {RuleNotification.YES: 'Y', RuleNotification.CLOSE: 'C', RuleNotification.PROGRESS: 'P'}.get(rule.notification, 'N')
1935
+
1936
+ new_rule_id = add_rule(dids=[{'scope': rule.scope, 'name': rule.name}],
1937
+ account=rule.account,
1938
+ copies=copies,
1939
+ rse_expression=rse_expression,
1940
+ grouping=grouping,
1941
+ weight=rule.weight,
1942
+ lifetime=lifetime,
1943
+ locked=rule.locked,
1944
+ subscription_id=rule.subscription_id,
1945
+ source_replica_expression=rule.source_replica_expression,
1946
+ activity=rule.activity,
1947
+ notify=notify,
1948
+ purge_replicas=rule.purge_replicas,
1949
+ ignore_availability=rule.ignore_availability,
1950
+ session=session)
1951
+
1952
+ session.flush()
1953
+
1954
+ stmt = select(
1955
+ models.ReplicationRule
1956
+ ).where(
1957
+ models.ReplicationRule.id == new_rule_id[0]
1958
+ )
1959
+ new_rule = session.execute(stmt).scalar_one()
1960
+
1961
+ if new_rule.state != RuleState.OK:
1962
+ raise RuleReplaceFailed('The replacement of the rule failed.')
1963
+
1964
+ delete_rule(rule_id=rule_id,
1965
+ session=session)
1966
+
1967
+ return new_rule_id[0]
1968
+
1969
+ except NoResultFound as exc:
1970
+ raise RuleNotFound('No rule with the id %s found' % (rule_id)) from exc
1971
+
1972
+
1973
+ @transactional_session
1974
+ def move_rule(
1975
+ rule_id: str,
1976
+ rse_expression: str,
1977
+ override: Optional[dict[str, Any]] = None,
1978
+ *,
1979
+ session: "Session"
1980
+ ) -> str:
1981
+ """
1982
+ Move a replication rule to another RSE and, once done, delete the original one.
1983
+
1984
+ :param rule_id: Rule to be moved.
1985
+ :param rse_expression: RSE expression of the new rule.
1986
+ :param override: Configurations to update for the new rule.
1987
+ :param session: The DB Session.
1988
+ :raises: RuleNotFound, RuleReplaceFailed, InvalidRSEExpression
1989
+ """
1990
+ override = override or {}
1991
+
1992
+ try:
1993
+ stmt = select(
1994
+ models.ReplicationRule
1995
+ ).where(
1996
+ models.ReplicationRule.id == rule_id
1997
+ )
1998
+ rule = session.execute(stmt).scalar_one()
1999
+
2000
+ if rule.child_rule_id:
2001
+ raise RuleReplaceFailed('The rule must not have a child rule.')
2002
+
2003
+ grouping = {RuleGrouping.ALL: 'ALL', RuleGrouping.NONE: 'NONE'}.get(rule.grouping, 'DATASET')
2004
+
2005
+ if rule.expires_at:
2006
+ lifetime = (rule.expires_at - datetime.utcnow()).days * 24 * 3600 + (rule.expires_at - datetime.utcnow()).seconds
2007
+ else:
2008
+ lifetime = None
2009
+
2010
+ notify = {RuleNotification.YES: 'Y', RuleNotification.CLOSE: 'C', RuleNotification.PROGRESS: 'P'}.get(rule.notification, 'N')
2011
+
2012
+ options = {
2013
+ 'dids': [{'scope': rule.scope, 'name': rule.name}],
2014
+ 'account': rule.account,
2015
+ 'copies': rule.copies,
2016
+ 'rse_expression': rse_expression,
2017
+ 'grouping': grouping,
2018
+ 'weight': rule.weight,
2019
+ 'lifetime': lifetime,
2020
+ 'locked': rule.locked,
2021
+ 'subscription_id': rule.subscription_id,
2022
+ 'source_replica_expression': rule.source_replica_expression,
2023
+ 'activity': rule.activity,
2024
+ 'notify': notify,
2025
+ 'purge_replicas': rule.purge_replicas,
2026
+ 'ignore_availability': rule.ignore_availability,
2027
+ 'comment': rule.comments,
2028
+ 'session': session,
2029
+ }
2030
+
2031
+ for key in override:
2032
+ if key in ['dids', 'session']:
2033
+ raise UnsupportedOperation('Not allowed to override option %s' % key)
2034
+ elif key not in options:
2035
+ raise UnsupportedOperation('Non-valid override option %s' % key)
2036
+ else:
2037
+ options[key] = override[key]
2038
+
2039
+ new_rule_id = add_rule(**options)
2040
+
2041
+ session.flush()
2042
+
2043
+ update_rule(rule_id=rule_id, options={'child_rule_id': new_rule_id[0], 'lifetime': 0}, session=session)
2044
+
2045
+ return new_rule_id[0]
2046
+
2047
+ except StatementError as exc:
2048
+ raise RucioException('Badly formatted rule id (%s)' % (rule_id)) from exc
2049
+ except NoResultFound as exc:
2050
+ raise RuleNotFound('No rule with the id %s found' % (rule_id)) from exc
2051
+
2052
+
2053
+ @transactional_session
2054
+ def re_evaluate_did(
2055
+ scope: InternalScope,
2056
+ name: str,
2057
+ rule_evaluation_action: DIDReEvaluation,
2058
+ *,
2059
+ session: "Session"
2060
+ ) -> None:
2061
+ """
2062
+ Re-Evaluates a did.
2063
+
2064
+ :param scope: The scope of the did to be re-evaluated.
2065
+ :param name: The name of the did to be re-evaluated.
2066
+ :param rule_evaluation_action: The Rule evaluation action.
2067
+ :param session: The database session in use.
2068
+ :raises: DataIdentifierNotFound
2069
+ """
2070
+
2071
+ try:
2072
+ stmt = select(
2073
+ models.DataIdentifier
2074
+ ).where(
2075
+ and_(models.DataIdentifier.scope == scope,
2076
+ models.DataIdentifier.name == name)
2077
+ )
2078
+ did = session.execute(stmt).scalar_one()
2079
+ except NoResultFound as exc:
2080
+ raise DataIdentifierNotFound() from exc
2081
+
2082
+ if rule_evaluation_action == DIDReEvaluation.ATTACH:
2083
+ __evaluate_did_attach(did, session=session)
2084
+ else:
2085
+ __evaluate_did_detach(did, session=session)
2086
+
2087
+ # Update size and length of did
2088
+ if session.bind.dialect.name == 'oracle':
2089
+ stmt = select(
2090
+ func.sum(models.DataIdentifierAssociation.bytes),
2091
+ func.count(1)
2092
+ ).with_hint(
2093
+ models.DataIdentifierAssociation, 'INDEX(CONTENTS CONTENTS_PK)', 'oracle'
2094
+ ).where(
2095
+ and_(models.DataIdentifierAssociation.scope == scope,
2096
+ models.DataIdentifierAssociation.name == name)
2097
+ )
2098
+ for bytes_, length in session.execute(stmt):
2099
+ did.bytes = bytes_
2100
+ did.length = length
2101
+
2102
+ # Add an updated_col_rep
2103
+ if did.did_type == DIDType.DATASET:
2104
+ models.UpdatedCollectionReplica(scope=scope,
2105
+ name=name,
2106
+ did_type=did.did_type).save(session=session)
2107
+
2108
+
2109
+ @read_session
2110
+ def get_updated_dids(
2111
+ total_workers: int,
2112
+ worker_number: int,
2113
+ limit: int = 100,
2114
+ blocked_dids: Optional['Sequence[tuple[str, str]]'] = None,
2115
+ *,
2116
+ session: "Session"
2117
+ ) -> list[tuple[str, InternalScope, str, DIDReEvaluation]]:
2118
+ """
2119
+ Get updated dids.
2120
+
2121
+ :param total_workers: Number of total workers.
2122
+ :param worker_number: id of the executing worker.
2123
+ :param limit: Maximum number of dids to return.
2124
+ :param blocked_dids: Blocked dids to filter.
2125
+ :param session: Database session in use.
2126
+ """
2127
+ blocked_dids = blocked_dids or []
2128
+ stmt = select(
2129
+ models.UpdatedDID.id,
2130
+ models.UpdatedDID.scope,
2131
+ models.UpdatedDID.name,
2132
+ models.UpdatedDID.rule_evaluation_action
2133
+ )
2134
+ stmt = filter_thread_work(session=session, query=stmt, total_threads=total_workers, thread_id=worker_number, hash_variable='name')
2135
+
2136
+ # Remove blocked dids from query, but only do the first 30 ones, not to overload the query
2137
+ if blocked_dids:
2138
+ chunk = list(chunks(blocked_dids, 30))[0]
2139
+ stmt = stmt.where(tuple_(models.UpdatedDID.scope, models.UpdatedDID.name).notin_(chunk))
2140
+
2141
+ if limit:
2142
+ fetched_dids = session.execute(stmt.order_by(models.UpdatedDID.created_at).limit(limit)).all()
2143
+ filtered_dids = [did._tuple() for did in fetched_dids if (did.scope, did.name) not in blocked_dids]
2144
+ if len(fetched_dids) == limit and not filtered_dids:
2145
+ return get_updated_dids(total_workers=total_workers,
2146
+ worker_number=worker_number,
2147
+ limit=None,
2148
+ blocked_dids=blocked_dids,
2149
+ session=session)
2150
+ else:
2151
+ return filtered_dids
2152
+ else:
2153
+ return [did._tuple() for did in session.execute(stmt.order_by(models.UpdatedDID.created_at)).all() if (did.scope, did.name) not in blocked_dids]
2154
+
2155
+
2156
+ @read_session
2157
+ def get_rules_beyond_eol(
2158
+ date_check: datetime,
2159
+ worker_number: int,
2160
+ total_workers: int, *,
2161
+ session: "Session"
2162
+ ) -> list[tuple[InternalScope,
2163
+ str,
2164
+ str,
2165
+ bool,
2166
+ str,
2167
+ Optional[datetime],
2168
+ Optional[datetime],
2169
+ InternalAccount]]:
2170
+ """
2171
+ Get rules which have eol_at before a certain date.
2172
+
2173
+ :param date_check: The reference date that should be compared to eol_at.
2174
+ :param worker_number: id of the executing worker.
2175
+ :param total_workers: Number of total workers.
2176
+ :param session: Database session in use.
2177
+ """
2178
+ stmt = select(
2179
+ models.ReplicationRule.scope,
2180
+ models.ReplicationRule.name,
2181
+ models.ReplicationRule.rse_expression,
2182
+ models.ReplicationRule.locked,
2183
+ models.ReplicationRule.id,
2184
+ models.ReplicationRule.eol_at,
2185
+ models.ReplicationRule.expires_at,
2186
+ models.ReplicationRule.account
2187
+ ).where(
2188
+ models.ReplicationRule.eol_at < date_check
2189
+ )
2190
+
2191
+ stmt = filter_thread_work(session=session, query=stmt, total_threads=total_workers, thread_id=worker_number, hash_variable='name')
2192
+ return [row._tuple() for row in session.execute(stmt).all()]
2193
+
2194
+
2195
+ @read_session
2196
+ def get_expired_rules(
2197
+ total_workers: int,
2198
+ worker_number: int,
2199
+ limit: int = 100,
2200
+ blocked_rules: Optional['Sequence[str]'] = None,
2201
+ *,
2202
+ session: "Session"
2203
+ ) -> list[tuple[str, str]]:
2204
+ """
2205
+ Get expired rules.
2206
+
2207
+ :param total_workers: Number of total workers.
2208
+ :param worker_number: id of the executing worker.
2209
+ :param limit: Maximum number of rules to return.
2210
+ :param blocked_rules: List of blocked rules.
2211
+ :param session: Database session in use.
2212
+ """
2213
+
2214
+ blocked_rules = blocked_rules or []
2215
+ stmt = select(
2216
+ models.ReplicationRule.id,
2217
+ models.ReplicationRule.rse_expression
2218
+ ).with_hint(
2219
+ models.ReplicationRule, 'INDEX(RULES RULES_EXPIRES_AT_IDX)', 'oracle'
2220
+ ).where(
2221
+ and_(models.ReplicationRule.expires_at < datetime.utcnow(),
2222
+ models.ReplicationRule.locked == false(),
2223
+ models.ReplicationRule.child_rule_id == null())
2224
+ ).order_by(
2225
+ models.ReplicationRule.expires_at
2226
+ )
2227
+ stmt = filter_thread_work(session=session, query=stmt, total_threads=total_workers, thread_id=worker_number, hash_variable='name')
2228
+
2229
+ if limit:
2230
+ stmt = stmt.limit(limit)
2231
+ result = session.execute(stmt).all()
2232
+ filtered_rules = [rule._tuple() for rule in result if rule.id not in blocked_rules]
2233
+ if len(result) == limit and not filtered_rules:
2234
+ return get_expired_rules(total_workers=total_workers,
2235
+ worker_number=worker_number,
2236
+ limit=None,
2237
+ blocked_rules=blocked_rules,
2238
+ session=session)
2239
+ else:
2240
+ return filtered_rules
2241
+ else:
2242
+ return [rule._tuple() for rule in session.execute(stmt).all() if rule.id not in blocked_rules]
2243
+
2244
+
2245
+ @read_session
2246
+ def get_injected_rules(
2247
+ total_workers: int,
2248
+ worker_number: int,
2249
+ limit: int = 100,
2250
+ blocked_rules: Optional['Sequence[str]'] = None,
2251
+ *,
2252
+ session: "Session"
2253
+ ) -> list[str]:
2254
+ """
2255
+ Get rules to be injected.
2256
+
2257
+ :param total_workers: Number of total workers.
2258
+ :param worker_number: id of the executing worker.
2259
+ :param limit: Maximum number of rules to return.
2260
+ :param blocked_rules: Blocked rules not to include.
2261
+ :param session: Database session in use.
2262
+ """
2263
+
2264
+ blocked_rules = blocked_rules or []
2265
+ stmt = select(
2266
+ models.ReplicationRule.id
2267
+ ).with_hint(
2268
+ models.ReplicationRule, 'INDEX(RULES RULES_STATE_IDX)', 'oracle'
2269
+ ).where(
2270
+ and_(models.ReplicationRule.state == RuleState.INJECT,
2271
+ models.ReplicationRule.created_at <= datetime.utcnow())
2272
+ ).order_by(
2273
+ models.ReplicationRule.created_at
2274
+ )
2275
+ stmt = filter_thread_work(session=session, query=stmt, total_threads=total_workers, thread_id=worker_number, hash_variable='name')
2276
+
2277
+ if limit:
2278
+ stmt = stmt.limit(limit)
2279
+ result = session.execute(stmt).scalars().all()
2280
+ filtered_rules = [rule for rule in result if rule not in blocked_rules]
2281
+ if len(result) == limit and not filtered_rules:
2282
+ return get_injected_rules(total_workers=total_workers,
2283
+ worker_number=worker_number,
2284
+ limit=None,
2285
+ blocked_rules=blocked_rules,
2286
+ session=session)
2287
+ else:
2288
+ return filtered_rules
2289
+ else:
2290
+ return [rule for rule in session.execute(stmt).scalars().all() if rule not in blocked_rules]
2291
+
2292
+
2293
+ @read_session
2294
+ def get_stuck_rules(
2295
+ total_workers: int,
2296
+ worker_number: int,
2297
+ delta: int = 600,
2298
+ limit: int = 10,
2299
+ blocked_rules: Optional['Sequence[str]'] = None,
2300
+ *,
2301
+ session: "Session"
2302
+ ) -> list[str]:
2303
+ """
2304
+ Get stuck rules.
2305
+
2306
+ :param total_workers: Number of total workers.
2307
+ :param worker_number: id of the executing worker.
2308
+ :param delta: Delta in seconds to select rules in.
2309
+ :param limit: Maximum number of rules to select.
2310
+ :param blocked_rules: Blocked rules to filter out.
2311
+ :param session: Database session in use.
2312
+ """
2313
+ blocked_rules = blocked_rules or []
2314
+ stmt = select(
2315
+ models.ReplicationRule.id
2316
+ ).with_hint(
2317
+ models.ReplicationRule, 'INDEX(RULES RULES_STATE_IDX)', 'oracle'
2318
+ ).where(
2319
+ and_(models.ReplicationRule.state == RuleState.STUCK,
2320
+ models.ReplicationRule.updated_at < datetime.utcnow() - timedelta(seconds=delta),
2321
+ or_(models.ReplicationRule.expires_at == null(),
2322
+ models.ReplicationRule.expires_at > datetime.utcnow(),
2323
+ models.ReplicationRule.locked == true()))
2324
+ ).order_by(
2325
+ models.ReplicationRule.updated_at
2326
+ )
2327
+ stmt = filter_thread_work(session=session, query=stmt, total_threads=total_workers, thread_id=worker_number, hash_variable='name')
2328
+
2329
+ if limit:
2330
+ stmt = stmt.limit(limit)
2331
+ result = session.execute(stmt).scalars().all()
2332
+ filtered_rules = [rule for rule in result if rule not in blocked_rules]
2333
+ if len(result) == limit and not filtered_rules:
2334
+ return get_stuck_rules(total_workers=total_workers,
2335
+ worker_number=worker_number,
2336
+ delta=delta,
2337
+ limit=None,
2338
+ blocked_rules=blocked_rules,
2339
+ session=session)
2340
+ else:
2341
+ return filtered_rules
2342
+ else:
2343
+ return [rule for rule in session.execute(stmt).scalars().all() if rule not in blocked_rules]
2344
+
2345
+
2346
+ @transactional_session
2347
+ def delete_updated_did(
2348
+ id_: str,
2349
+ *,
2350
+ session: "Session"
2351
+ ) -> None:
2352
+ """
2353
+ Delete an updated_did by id.
2354
+
2355
+ :param id_: Id of the row not to delete.
2356
+ :param session: The database session in use.
2357
+ """
2358
+ stmt = delete(
2359
+ models.UpdatedDID
2360
+ ).where(
2361
+ models.UpdatedDID.id == id_
2362
+ )
2363
+ session.execute(stmt)
2364
+
2365
+
2366
+ @transactional_session
2367
+ def update_rules_for_lost_replica(
2368
+ scope: InternalScope,
2369
+ name: str,
2370
+ rse_id: str,
2371
+ nowait: bool = False,
2372
+ *,
2373
+ session: "Session",
2374
+ logger: LoggerFunction = logging.log
2375
+ ) -> None:
2376
+ """
2377
+ Update rules if a file replica is lost.
2378
+
2379
+ :param scope: Scope of the replica.
2380
+ :param name: Name of the replica.
2381
+ :param rse_id: RSE id of the replica.
2382
+ :param nowait: Nowait parameter for the FOR UPDATE statement.
2383
+ :param session: The database session in use.
2384
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
2385
+ """
2386
+
2387
+ stmt = select(
2388
+ models.ReplicaLock
2389
+ ).where(
2390
+ and_(models.ReplicaLock.scope == scope,
2391
+ models.ReplicaLock.name == name,
2392
+ models.ReplicaLock.rse_id == rse_id)
2393
+ ).with_for_update(
2394
+ nowait=nowait
2395
+ )
2396
+ locks = session.execute(stmt).scalars().all()
2397
+
2398
+ stmt = select(
2399
+ models.RSEFileAssociation
2400
+ ).where(
2401
+ and_(models.RSEFileAssociation.scope == scope,
2402
+ models.RSEFileAssociation.name == name,
2403
+ models.RSEFileAssociation.rse_id == rse_id)
2404
+ ).with_for_update(
2405
+ nowait=nowait
2406
+ )
2407
+ replica = session.execute(stmt).scalar_one()
2408
+
2409
+ stmt = select(
2410
+ models.Request
2411
+ ).where(
2412
+ and_(models.Request.scope == scope,
2413
+ models.Request.name == name,
2414
+ models.Request.dest_rse_id == rse_id)
2415
+ ).with_for_update(
2416
+ nowait=nowait
2417
+ )
2418
+ requests = session.execute(stmt).scalars().all()
2419
+
2420
+ rse = get_rse_name(rse_id, session=session)
2421
+
2422
+ datasets = []
2423
+ parent_dids = rucio.core.did.list_parent_dids(scope=scope, name=name, session=session)
2424
+ for parent in parent_dids:
2425
+ if {'name': parent['name'], 'scope': parent['scope']} not in datasets:
2426
+ datasets.append({'name': parent['name'], 'scope': parent['scope']})
2427
+
2428
+ for request in requests:
2429
+ session.delete(request)
2430
+
2431
+ for lock in locks:
2432
+ stmt = select(
2433
+ models.ReplicationRule
2434
+ ).where(
2435
+ models.ReplicationRule.id == lock.rule_id
2436
+ ).with_for_update(
2437
+ nowait=nowait
2438
+ )
2439
+ rule = session.execute(stmt).scalar_one()
2440
+ rule_state_before = rule.state
2441
+ replica.lock_cnt -= 1
2442
+ if lock.state == LockState.OK:
2443
+ rule.locks_ok_cnt -= 1
2444
+ elif lock.state == LockState.REPLICATING:
2445
+ rule.locks_replicating_cnt -= 1
2446
+ elif lock.state == LockState.STUCK:
2447
+ rule.locks_stuck_cnt -= 1
2448
+ account_counter.decrease(rse_id=rse_id, account=rule.account, files=1, bytes_=lock.bytes, session=session)
2449
+ if rule.state == RuleState.SUSPENDED:
2450
+ pass
2451
+ elif rule.state == RuleState.STUCK:
2452
+ pass
2453
+ elif rule.locks_replicating_cnt == 0 and rule.locks_stuck_cnt == 0:
2454
+ rule.state = RuleState.OK
2455
+ if rule.grouping != RuleGrouping.NONE:
2456
+ stmt = update(
2457
+ models.DatasetLock
2458
+ ).where(
2459
+ models.DatasetLock.rule_id == rule.id
2460
+ ).values({
2461
+ models.DatasetLock.state: LockState.OK
2462
+ })
2463
+ session.execute(stmt)
2464
+ session.flush()
2465
+ if rule_state_before != RuleState.OK:
2466
+ generate_rule_notifications(rule=rule, session=session)
2467
+ generate_email_for_rule_ok_notification(rule=rule, session=session)
2468
+ # Try to release potential parent rules
2469
+ release_parent_rule(child_rule_id=rule.id, session=session)
2470
+ # Insert rule history
2471
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
2472
+
2473
+ session.delete(lock)
2474
+
2475
+ if replica.lock_cnt != 0:
2476
+ logger(logging.ERROR, 'Replica for did %s:%s with lock_cnt = %s. This should never happen. Update lock_cnt', scope, name, replica.lock_cnt)
2477
+ replica.lock_cnt = 0
2478
+
2479
+ replica.tombstone = OBSOLETE
2480
+ replica.state = ReplicaState.UNAVAILABLE
2481
+ stmt = update(
2482
+ models.DataIdentifier
2483
+ ).where(
2484
+ and_(models.DataIdentifier.scope == scope,
2485
+ models.DataIdentifier.name == name)
2486
+ ).values({
2487
+ models.DataIdentifier.availability: DIDAvailability.LOST
2488
+ })
2489
+ session.execute(stmt)
2490
+
2491
+ stmt = update(
2492
+ models.BadReplica
2493
+ ).where(
2494
+ and_(models.BadReplica.scope == scope,
2495
+ models.BadReplica.name == name,
2496
+ models.BadReplica.rse_id == rse_id,
2497
+ models.BadReplica.state == BadFilesStatus.BAD)
2498
+ ).values({
2499
+ models.BadReplica.state: BadFilesStatus.LOST,
2500
+ models.BadReplica.updated_at: datetime.utcnow()
2501
+ })
2502
+ session.execute(stmt)
2503
+ for dts in datasets:
2504
+ logger(logging.INFO, 'File %s:%s bad at site %s is completely lost from dataset %s:%s. Will be marked as LOST and detached', scope, name, rse, dts['scope'], dts['name'])
2505
+ rucio.core.did.detach_dids(scope=dts['scope'], name=dts['name'], dids=[{'scope': scope, 'name': name}], session=session)
2506
+
2507
+ message = {'scope': scope.external,
2508
+ 'name': name,
2509
+ 'dataset_name': dts['name'],
2510
+ 'dataset_scope': dts['scope'].external}
2511
+ if scope.vo != 'def':
2512
+ message['vo'] = scope.vo
2513
+
2514
+ add_message('LOST', message, session=session)
2515
+
2516
+
2517
+ @transactional_session
2518
+ def update_rules_for_bad_replica(
2519
+ scope: InternalScope,
2520
+ name: str,
2521
+ rse_id: str,
2522
+ nowait: bool = False,
2523
+ *,
2524
+ session: "Session",
2525
+ logger: LoggerFunction = logging.log
2526
+ ) -> None:
2527
+ """
2528
+ Update rules if a file replica is bad and has to be recreated.
2529
+
2530
+ :param scope: Scope of the replica.
2531
+ :param name: Name of the replica.
2532
+ :param rse_id: RSE id of the replica.
2533
+ :param nowait: Nowait parameter for the FOR UPDATE statement.
2534
+ :param session: The database session in use.
2535
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
2536
+ """
2537
+ stmt = select(
2538
+ models.ReplicaLock
2539
+ ).where(
2540
+ and_(models.ReplicaLock.scope == scope,
2541
+ models.ReplicaLock.name == name,
2542
+ models.ReplicaLock.rse_id == rse_id)
2543
+ ).with_for_update(
2544
+ nowait=nowait
2545
+ )
2546
+ locks = session.execute(stmt).scalars().all()
2547
+
2548
+ stmt = select(
2549
+ models.RSEFileAssociation
2550
+ ).where(
2551
+ and_(models.RSEFileAssociation.scope == scope,
2552
+ models.RSEFileAssociation.name == name,
2553
+ models.RSEFileAssociation.rse_id == rse_id)
2554
+ ).with_for_update(
2555
+ nowait=nowait
2556
+ )
2557
+ replica = session.execute(stmt).scalar_one()
2558
+
2559
+ nlock = 0
2560
+ datasets = []
2561
+ for lock in locks:
2562
+ nlock += 1
2563
+ stmt = select(
2564
+ models.ReplicationRule
2565
+ ).where(
2566
+ models.ReplicationRule.id == lock.rule_id
2567
+ ).with_for_update(
2568
+ nowait=nowait
2569
+ )
2570
+ rule = session.execute(stmt).scalar_one()
2571
+ # If source replica expression exists, we remove it
2572
+ if rule.source_replica_expression:
2573
+ rule.source_replica_expression = None
2574
+ # Get the affected datasets
2575
+ ds_scope = rule.scope
2576
+ ds_name = rule.name
2577
+ dataset = '%s:%s' % (ds_scope, ds_name)
2578
+ if dataset not in datasets:
2579
+ datasets.append(dataset)
2580
+ logger(logging.INFO, 'Recovering file %s:%s from dataset %s:%s at site %s', scope, name, ds_scope, ds_name, get_rse_name(rse_id=rse_id, session=session))
2581
+ # Insert a new row in the UpdateCollectionReplica table
2582
+ models.UpdatedCollectionReplica(scope=ds_scope,
2583
+ name=ds_name,
2584
+ did_type=rule.did_type,
2585
+ rse_id=lock.rse_id).save(flush=False, session=session)
2586
+ # Set the lock counters
2587
+ if lock.state == LockState.OK:
2588
+ rule.locks_ok_cnt -= 1
2589
+ elif lock.state == LockState.REPLICATING:
2590
+ rule.locks_replicating_cnt -= 1
2591
+ elif lock.state == LockState.STUCK:
2592
+ rule.locks_stuck_cnt -= 1
2593
+ rule.locks_replicating_cnt += 1
2594
+ # Generate the request
2595
+ try:
2596
+ request_core.get_request_by_did(scope, name, rse_id, session=session)
2597
+ except RequestNotFound:
2598
+ bytes_ = replica.bytes
2599
+ md5 = replica.md5
2600
+ adler32 = replica.adler32
2601
+ request_core.queue_requests(requests=[create_transfer_dict(dest_rse_id=rse_id,
2602
+ request_type=RequestType.TRANSFER,
2603
+ scope=scope, name=name, rule=rule, lock=lock, bytes_=bytes_, md5=md5, adler32=adler32,
2604
+ ds_scope=ds_scope, ds_name=ds_name, copy_pin_lifetime=None, activity='Recovery', session=session)], session=session)
2605
+ lock.state = LockState.REPLICATING
2606
+ if rule.state == RuleState.SUSPENDED:
2607
+ pass
2608
+ elif rule.state == RuleState.STUCK:
2609
+ pass
2610
+ else:
2611
+ rule.state = RuleState.REPLICATING
2612
+ if rule.grouping != RuleGrouping.NONE:
2613
+ stmt = update(
2614
+ models.DatasetLock
2615
+ ).where(
2616
+ models.DatasetLock.rule_id == rule.id
2617
+ ).values({
2618
+ models.DatasetLock.state: LockState.REPLICATING
2619
+ })
2620
+ session.execute(stmt)
2621
+ # Insert rule history
2622
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
2623
+ if nlock:
2624
+ stmt = update(
2625
+ models.RSEFileAssociation
2626
+ ).where(
2627
+ and_(models.RSEFileAssociation.scope == scope,
2628
+ models.RSEFileAssociation.name == name,
2629
+ models.RSEFileAssociation.rse_id == rse_id)
2630
+ ).values({
2631
+ models.RSEFileAssociation.state: ReplicaState.COPYING
2632
+ })
2633
+ session.execute(stmt)
2634
+ else:
2635
+ logger(logging.INFO, 'File %s:%s at site %s has no locks. Will be deleted now.', scope, name, get_rse_name(rse_id=rse_id, session=session))
2636
+ tombstone = OBSOLETE
2637
+ stmt = update(
2638
+ models.RSEFileAssociation
2639
+ ).where(
2640
+ and_(models.RSEFileAssociation.scope == scope,
2641
+ models.RSEFileAssociation.name == name,
2642
+ models.RSEFileAssociation.rse_id == rse_id)
2643
+ ).values({
2644
+ models.RSEFileAssociation.state: ReplicaState.UNAVAILABLE,
2645
+ models.RSEFileAssociation.tombstone: tombstone
2646
+ })
2647
+ session.execute(stmt)
2648
+
2649
+
2650
+ @transactional_session
2651
+ def generate_rule_notifications(
2652
+ rule: models.ReplicationRule,
2653
+ replicating_locks_before: Optional[int] = None,
2654
+ *,
2655
+ session: "Session"
2656
+ ) -> None:
2657
+ """
2658
+ Generate (If necessary) a callback for a rule (DATASETLOCK_OK, RULE_OK, DATASETLOCK_PROGRESS)
2659
+
2660
+ :param rule: The rule object.
2661
+ :param replicating_locks_before: Amount of replicating locks before the current state change.
2662
+ :param session: The Database session
2663
+ """
2664
+
2665
+ session.flush()
2666
+ total_locks = rule.locks_replicating_cnt + rule.locks_ok_cnt
2667
+
2668
+ if rule.state == RuleState.OK:
2669
+ # Only notify when rule is in state OK
2670
+
2671
+ # RULE_OK RULE_PROGRESS NOTIFICATIONS:
2672
+ if rule.notification == RuleNotification.YES:
2673
+ payload = {'scope': rule.scope.external,
2674
+ 'name': rule.name,
2675
+ 'rule_id': rule.id}
2676
+ if rule.scope.vo != 'def':
2677
+ payload['vo'] = rule.scope.vo
2678
+
2679
+ add_message(event_type='RULE_OK', payload=payload, session=session)
2680
+
2681
+ elif rule.notification in [RuleNotification.CLOSE, RuleNotification.PROGRESS]:
2682
+ try:
2683
+ did = rucio.core.did.get_did(scope=rule.scope, name=rule.name, session=session)
2684
+ if not did['open']:
2685
+ payload = {'scope': rule.scope.external,
2686
+ 'name': rule.name,
2687
+ 'rule_id': rule.id}
2688
+ if rule.scope.vo != 'def':
2689
+ payload['vo'] = rule.scope.vo
2690
+
2691
+ add_message(event_type='RULE_OK', payload=payload, session=session)
2692
+
2693
+ if rule.notification == RuleNotification.PROGRESS:
2694
+ payload = {'scope': rule.scope.external,
2695
+ 'name': rule.name,
2696
+ 'rule_id': rule.id,
2697
+ 'progress': __progress_class(rule.locks_replicating_cnt, total_locks)}
2698
+ if rule.scope.vo != 'def':
2699
+ payload['vo'] = rule.scope.vo
2700
+
2701
+ add_message(event_type='RULE_PROGRESS', payload=payload, session=session)
2702
+
2703
+ except DataIdentifierNotFound:
2704
+ pass
2705
+
2706
+ # DATASETLOCK_OK NOTIFICATIONS:
2707
+ if rule.grouping != RuleGrouping.NONE:
2708
+ # Only send DATASETLOCK_OK callbacks for ALL/DATASET grouped rules
2709
+ if rule.notification == RuleNotification.YES:
2710
+ stmt = select(
2711
+ models.DatasetLock
2712
+ ).where(
2713
+ models.DatasetLock.rule_id == rule.id
2714
+ )
2715
+ dataset_locks = session.execute(stmt).scalars().all()
2716
+ for dataset_lock in dataset_locks:
2717
+ payload = {'scope': dataset_lock.scope.external,
2718
+ 'name': dataset_lock.name,
2719
+ 'rse': get_rse_name(rse_id=dataset_lock.rse_id, session=session),
2720
+ 'rse_id': dataset_lock.rse_id,
2721
+ 'rule_id': rule.id}
2722
+ if dataset_lock.scope.vo != 'def':
2723
+ payload['vo'] = dataset_lock.scope.vo
2724
+
2725
+ add_message(event_type='DATASETLOCK_OK', payload=payload, session=session)
2726
+
2727
+ elif rule.notification == RuleNotification.CLOSE:
2728
+ stmt = select(
2729
+ models.DatasetLock
2730
+ ).where(
2731
+ models.DatasetLock.rule_id == rule.id
2732
+ )
2733
+ dataset_locks = session.execute(stmt).scalars().all()
2734
+ for dataset_lock in dataset_locks:
2735
+ try:
2736
+ did = rucio.core.did.get_did(scope=dataset_lock.scope, name=dataset_lock.name, session=session)
2737
+ if not did['open']:
2738
+ if did['length'] is None:
2739
+ return
2740
+ if did['length'] * rule.copies == rule.locks_ok_cnt:
2741
+ payload = {'scope': dataset_lock.scope.external,
2742
+ 'name': dataset_lock.name,
2743
+ 'rse': get_rse_name(rse_id=dataset_lock.rse_id, session=session),
2744
+ 'rse_id': dataset_lock.rse_id,
2745
+ 'rule_id': rule.id}
2746
+ if dataset_lock.scope.vo != 'def':
2747
+ payload['vo'] = dataset_lock.scope.vo
2748
+
2749
+ add_message(event_type='DATASETLOCK_OK', payload=payload, session=session)
2750
+
2751
+ except DataIdentifierNotFound:
2752
+ pass
2753
+
2754
+ elif rule.state == RuleState.REPLICATING and rule.notification == RuleNotification.PROGRESS and replicating_locks_before:
2755
+ # For RuleNotification PROGRESS rules, also notify when REPLICATING thresholds are passed
2756
+ if __progress_class(replicating_locks_before, total_locks) != __progress_class(rule.locks_replicating_cnt, total_locks):
2757
+ try:
2758
+ did = rucio.core.did.get_did(scope=rule.scope, name=rule.name, session=session)
2759
+ if not did['open']:
2760
+ payload = {'scope': rule.scope.external,
2761
+ 'name': rule.name,
2762
+ 'rule_id': rule.id,
2763
+ 'progress': __progress_class(rule.locks_replicating_cnt, total_locks)}
2764
+ if rule.scope.vo != 'def':
2765
+ payload['vo'] = rule.scope.vo
2766
+
2767
+ add_message(event_type='RULE_PROGRESS', payload=payload, session=session)
2768
+
2769
+ except DataIdentifierNotFound:
2770
+ pass
2771
+
2772
+
2773
+ @transactional_session
2774
+ def generate_email_for_rule_ok_notification(
2775
+ rule: models.ReplicationRule,
2776
+ *,
2777
+ session: "Session",
2778
+ logger: LoggerFunction = logging.log
2779
+ ) -> None:
2780
+ """
2781
+ Generate (If necessary) an eMail for a rule with notification mode Y.
2782
+
2783
+ :param rule: The rule object.
2784
+ :param session: The Database session
2785
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
2786
+ """
2787
+
2788
+ session.flush()
2789
+
2790
+ if rule.state == RuleState.OK and rule.notification == RuleNotification.YES:
2791
+ try:
2792
+ template_path = '%s/rule_ok_notification.tmpl' % config_get('common', 'mailtemplatedir')
2793
+ except NoOptionError as ex:
2794
+ logger(logging.ERROR, "Missing configuration option 'mailtemplatedir'.", exc_info=ex)
2795
+ return
2796
+
2797
+ try:
2798
+ with open(template_path, 'r') as templatefile:
2799
+ template = Template(templatefile.read())
2800
+ except OSError as ex:
2801
+ logger(logging.ERROR, "Couldn't open file '%s'", template_path, exc_info=ex)
2802
+ return
2803
+
2804
+ email = get_account(account=rule.account, session=session).email
2805
+ if not email:
2806
+ logger(logging.INFO, 'No email associated with rule ID %s.', rule.id)
2807
+ return
2808
+
2809
+ try:
2810
+ email_body = template.safe_substitute({'rule_id': str(rule.id),
2811
+ 'created_at': str(rule.created_at),
2812
+ 'expires_at': str(rule.expires_at),
2813
+ 'rse_expression': rule.rse_expression,
2814
+ 'comment': rule.comments,
2815
+ 'scope': rule.scope.external,
2816
+ 'name': rule.name,
2817
+ 'did_type': rule.did_type})
2818
+ except ValueError as ex:
2819
+ logger(logging.ERROR, "Invalid mail template.", exc_info=ex)
2820
+ return
2821
+
2822
+ add_message(event_type='email',
2823
+ payload={'body': email_body,
2824
+ 'to': [email],
2825
+ 'subject': '[RUCIO] Replication rule %s has been successfully transferred' % (str(rule.id))},
2826
+ session=session)
2827
+
2828
+
2829
+ @transactional_session
2830
+ def insert_rule_history(
2831
+ rule: models.ReplicationRule,
2832
+ recent: bool = True,
2833
+ longterm: bool = False,
2834
+ *,
2835
+ session: "Session"
2836
+ ) -> None:
2837
+ """
2838
+ Insert rule history to recent/longterm history.
2839
+
2840
+ :param rule: The rule object.
2841
+ :param recent: Insert to recent table.
2842
+ :param longterm: Insert to longterm table.
2843
+ :param session: The Database session.
2844
+ """
2845
+ if recent:
2846
+ models.ReplicationRuleHistoryRecent(id=rule.id, subscription_id=rule.subscription_id, account=rule.account, scope=rule.scope, name=rule.name,
2847
+ did_type=rule.did_type, state=rule.state, error=rule.error, rse_expression=rule.rse_expression, copies=rule.copies,
2848
+ expires_at=rule.expires_at, weight=rule.weight, locked=rule.locked, locks_ok_cnt=rule.locks_ok_cnt,
2849
+ locks_replicating_cnt=rule.locks_replicating_cnt, locks_stuck_cnt=rule.locks_stuck_cnt, source_replica_expression=rule.source_replica_expression,
2850
+ activity=rule.activity, grouping=rule.grouping, notification=rule.notification, stuck_at=rule.stuck_at, purge_replicas=rule.purge_replicas,
2851
+ ignore_availability=rule.ignore_availability, ignore_account_limit=rule.ignore_account_limit, comments=rule.comments, created_at=rule.created_at,
2852
+ updated_at=rule.updated_at, child_rule_id=rule.child_rule_id, eol_at=rule.eol_at,
2853
+ split_container=rule.split_container, meta=rule.meta).save(session=session)
2854
+ if longterm:
2855
+ models.ReplicationRuleHistory(id=rule.id, subscription_id=rule.subscription_id, account=rule.account, scope=rule.scope, name=rule.name,
2856
+ did_type=rule.did_type, state=rule.state, error=rule.error, rse_expression=rule.rse_expression, copies=rule.copies,
2857
+ expires_at=rule.expires_at, weight=rule.weight, locked=rule.locked, locks_ok_cnt=rule.locks_ok_cnt,
2858
+ locks_replicating_cnt=rule.locks_replicating_cnt, locks_stuck_cnt=rule.locks_stuck_cnt, source_replica_expression=rule.source_replica_expression,
2859
+ activity=rule.activity, grouping=rule.grouping, notification=rule.notification, stuck_at=rule.stuck_at, purge_replicas=rule.purge_replicas,
2860
+ ignore_availability=rule.ignore_availability, ignore_account_limit=rule.ignore_account_limit, comments=rule.comments, created_at=rule.created_at,
2861
+ updated_at=rule.updated_at, child_rule_id=rule.child_rule_id, eol_at=rule.eol_at,
2862
+ split_container=rule.split_container, meta=rule.meta).save(session=session)
2863
+
2864
+
2865
+ @transactional_session
2866
+ def approve_rule(
2867
+ rule_id: str,
2868
+ approver: str = '',
2869
+ notify_approvers: bool = True,
2870
+ *,
2871
+ session: "Session"
2872
+ ) -> None:
2873
+ """
2874
+ Approve a specific replication rule.
2875
+
2876
+ :param rule_id: The rule_id to approve.
2877
+ :param approver: The account which is approving the rule.
2878
+ :param notify_approvers: Notify the other approvers.
2879
+ :param session: The database session in use.
2880
+ :raises: RuleNotFound if no Rule can be found.
2881
+ """
2882
+
2883
+ try:
2884
+ stmt = select(
2885
+ models.ReplicationRule
2886
+ ).where(
2887
+ models.ReplicationRule.id == rule_id
2888
+ )
2889
+ rule = session.execute(stmt).scalar_one()
2890
+ if rule.state == RuleState.WAITING_APPROVAL:
2891
+ rule.ignore_account_limit = True
2892
+ rule.state = RuleState.INJECT
2893
+ if approver:
2894
+ approver_email = get_account(account=approver, session=session).email
2895
+ if approver_email:
2896
+ approver = '%s (%s)' % (approver, approver_email)
2897
+ else:
2898
+ approver = 'AUTOMATIC'
2899
+ with open('%s/rule_approved_user.tmpl' % config_get('common', 'mailtemplatedir'), 'r') as templatefile:
2900
+ template = Template(templatefile.read())
2901
+ email = get_account(account=rule.account, session=session).email
2902
+ if email:
2903
+ text = template.safe_substitute({'rule_id': str(rule.id),
2904
+ 'expires_at': str(rule.expires_at),
2905
+ 'rse_expression': rule.rse_expression,
2906
+ 'comment': rule.comments,
2907
+ 'scope': rule.scope.external,
2908
+ 'name': rule.name,
2909
+ 'did_type': rule.did_type,
2910
+ 'approver': approver})
2911
+ add_message(event_type='email',
2912
+ payload={'body': text,
2913
+ 'to': [email],
2914
+ 'subject': '[RUCIO] Replication rule %s has been approved' % (str(rule.id))},
2915
+ session=session)
2916
+ # Also notify the other approvers
2917
+ if notify_approvers:
2918
+ with open('%s/rule_approved_admin.tmpl' % config_get('common', 'mailtemplatedir'), 'r') as templatefile:
2919
+ template = Template(templatefile.read())
2920
+ text = template.safe_substitute({'rule_id': str(rule.id),
2921
+ 'approver': approver})
2922
+ vo = rule.account.vo
2923
+ recipients = _create_recipients_list(rse_expression=rule.rse_expression, filter_={'vo': vo}, session=session)
2924
+ for recipient in recipients:
2925
+ add_message(event_type='email',
2926
+ payload={'body': text,
2927
+ 'to': [recipient[0]],
2928
+ 'subject': 'Re: [RUCIO] Request to approve replication rule %s' % (str(rule.id))},
2929
+ session=session)
2930
+ except NoResultFound as exc:
2931
+ raise RuleNotFound('No rule with the id %s found' % (rule_id)) from exc
2932
+ except StatementError as exc:
2933
+ raise RucioException('Badly formatted rule id (%s)' % (rule_id)) from exc
2934
+
2935
+
2936
+ @transactional_session
2937
+ def deny_rule(
2938
+ rule_id: str,
2939
+ approver: str = '',
2940
+ reason: Optional[str] = None,
2941
+ *,
2942
+ session: "Session"
2943
+ ) -> None:
2944
+ """
2945
+ Deny a specific replication rule.
2946
+
2947
+ :param rule_id: The rule_id to approve.
2948
+ :param approver: The account which is denying the rule.
2949
+ :param reason: The reason why the rule was denied.
2950
+ :param session: The database session in use.
2951
+ :raises: RuleNotFound if no Rule can be found.
2952
+ """
2953
+
2954
+ try:
2955
+ stmt = select(
2956
+ models.ReplicationRule
2957
+ ).where(
2958
+ models.ReplicationRule.id == rule_id
2959
+ )
2960
+ rule = session.execute(stmt).scalar_one()
2961
+ if rule.state == RuleState.WAITING_APPROVAL:
2962
+ with open('%s/rule_denied_user.tmpl' % config_get('common', 'mailtemplatedir'), 'r') as templatefile:
2963
+ template = Template(templatefile.read())
2964
+ email = get_account(account=rule.account, session=session).email
2965
+ if approver:
2966
+ approver_email = get_account(account=approver, session=session).email
2967
+ if approver_email:
2968
+ approver = '%s (%s)' % (approver, approver_email)
2969
+ else:
2970
+ approver = 'AUTOMATIC'
2971
+ if email:
2972
+ email_body = template.safe_substitute({'rule_id': str(rule.id),
2973
+ 'rse_expression': rule.rse_expression,
2974
+ 'comment': rule.comments,
2975
+ 'scope': rule.scope.external,
2976
+ 'name': rule.name,
2977
+ 'did_type': rule.did_type,
2978
+ 'approver': approver,
2979
+ 'reason': reason})
2980
+ add_message(event_type='email',
2981
+ payload={'body': email_body,
2982
+ 'to': [email],
2983
+ 'subject': '[RUCIO] Replication rule %s has been denied' % (str(rule.id))},
2984
+ session=session)
2985
+ delete_rule(rule_id=rule_id, ignore_rule_lock=True, session=session)
2986
+ # Also notify the other approvers
2987
+ with open('%s/rule_denied_admin.tmpl' % config_get('common', 'mailtemplatedir'), 'r') as templatefile:
2988
+ template = Template(templatefile.read())
2989
+ email_body = template.safe_substitute({'rule_id': str(rule.id),
2990
+ 'approver': approver,
2991
+ 'reason': reason})
2992
+ vo = rule.account.vo
2993
+ recipients = _create_recipients_list(rse_expression=rule.rse_expression, filter_={'vo': vo}, session=session)
2994
+ for recipient in recipients:
2995
+ add_message(event_type='email',
2996
+ payload={'body': email_body,
2997
+ 'to': [recipient[0]],
2998
+ 'subject': 'Re: [RUCIO] Request to approve replication rule %s' % (str(rule.id))},
2999
+ session=session)
3000
+ except NoResultFound as exc:
3001
+ raise RuleNotFound('No rule with the id %s found' % rule_id) from exc
3002
+ except StatementError as exc:
3003
+ raise RucioException('Badly formatted rule id (%s)' % rule_id) from exc
3004
+
3005
+
3006
+ @transactional_session
3007
+ def examine_rule(
3008
+ rule_id: str,
3009
+ *,
3010
+ session: "Session"
3011
+ ) -> dict[str, Any]:
3012
+ """
3013
+ Examine a replication rule for transfer errors.
3014
+
3015
+ :param rule_id: Replication rule id
3016
+ :param session: Session of the db.
3017
+ :returns: Dictionary of information
3018
+ """
3019
+ result = {'rule_error': None,
3020
+ 'transfers': []}
3021
+
3022
+ try:
3023
+ stmt = select(
3024
+ models.ReplicationRule
3025
+ ).where(
3026
+ models.ReplicationRule.id == rule_id
3027
+ )
3028
+ rule = session.execute(stmt).scalar_one()
3029
+ if rule.state == RuleState.OK:
3030
+ result['rule_error'] = 'This replication rule is OK'
3031
+ elif rule.state == RuleState.REPLICATING:
3032
+ result['rule_error'] = 'This replication rule is currently REPLICATING'
3033
+ elif rule.state == RuleState.SUSPENDED:
3034
+ result['rule_error'] = 'This replication rule is SUSPENDED'
3035
+ else:
3036
+ result['rule_error'] = rule.error
3037
+ # Get the stuck locks
3038
+ stmt = select(
3039
+ models.ReplicaLock
3040
+ ).where(
3041
+ and_(models.ReplicaLock.rule_id == rule_id,
3042
+ models.ReplicaLock.state == LockState.STUCK)
3043
+ )
3044
+ stuck_locks = session.execute(stmt).scalars().all()
3045
+ for lock in stuck_locks:
3046
+ # Get the count of requests in the request_history for each lock
3047
+ stmt = select(
3048
+ models.RequestHistory
3049
+ ).where(
3050
+ and_(models.RequestHistory.scope == lock.scope,
3051
+ models.RequestHistory.name == lock.name,
3052
+ models.RequestHistory.dest_rse_id == lock.rse_id)
3053
+ ).order_by(
3054
+ desc(models.RequestHistory.created_at)
3055
+ )
3056
+ transfers = session.execute(stmt).scalars().all()
3057
+ transfer_cnt = len(transfers)
3058
+ # Get the error of the last request that has been tried and also the SOURCE used for the last request
3059
+ last_error, last_source, last_time, sources = None, None, None, []
3060
+ if transfers:
3061
+ last_request = transfers[0]
3062
+ last_error = last_request.state
3063
+ last_time = last_request.created_at
3064
+ last_source = None if last_request.source_rse_id is None else get_rse_name(rse_id=last_request.source_rse_id, session=session)
3065
+ stmt = select(
3066
+ models.RSEFileAssociation
3067
+ ).where(
3068
+ and_(models.RSEFileAssociation.scope == lock.scope,
3069
+ models.RSEFileAssociation.name == lock.name,
3070
+ models.RSEFileAssociation.state == ReplicaState.AVAILABLE)
3071
+ )
3072
+ available_replicas = session.execute(stmt).scalars().all()
3073
+
3074
+ for replica in available_replicas:
3075
+ sources.append((get_rse_name(rse_id=replica.rse_id, session=session),
3076
+ True if get_rse(rse_id=replica.rse_id, session=session)['availability_read'] else False))
3077
+
3078
+ result['transfers'].append({'scope': lock.scope,
3079
+ 'name': lock.name,
3080
+ 'rse_id': lock.rse_id,
3081
+ 'rse': get_rse_name(rse_id=lock.rse_id, session=session),
3082
+ 'attempts': transfer_cnt,
3083
+ 'last_error': str(last_error),
3084
+ 'last_source': last_source,
3085
+ 'sources': sources,
3086
+ 'last_time': last_time})
3087
+ return result
3088
+ except NoResultFound as exc:
3089
+ raise RuleNotFound('No rule with the id %s found' % (rule_id)) from exc
3090
+ except StatementError as exc:
3091
+ raise RucioException('Badly formatted rule id (%s)' % (rule_id)) from exc
3092
+
3093
+
3094
+ @transactional_session
3095
+ def get_evaluation_backlog(
3096
+ expiration_time: int = 600,
3097
+ *,
3098
+ session: "Session"
3099
+ ) -> tuple[int, datetime]:
3100
+ """
3101
+ Counts the number of entries in the rule evaluation backlog.
3102
+ (Number of files to be evaluated)
3103
+
3104
+ :returns: Tuple (Count, Datetime of oldest entry)
3105
+ """
3106
+
3107
+ cached_backlog: Union[NoValue, tuple[int, datetime]] = REGION.get('rule_evaluation_backlog', expiration_time=expiration_time)
3108
+ if isinstance(cached_backlog, NoValue):
3109
+ stmt = select(
3110
+ func.count(models.UpdatedDID.created_at),
3111
+ func.min(models.UpdatedDID.created_at)
3112
+ )
3113
+ result = session.execute(stmt).one()._tuple()
3114
+ REGION.set('rule_evaluation_backlog', result)
3115
+ return result
3116
+ return cached_backlog
3117
+
3118
+
3119
+ @transactional_session
3120
+ def release_parent_rule(
3121
+ child_rule_id: str,
3122
+ remove_parent_expiration: bool = False,
3123
+ *,
3124
+ session: "Session"
3125
+ ) -> None:
3126
+ """
3127
+ Release a potential parent rule, because the child_rule is OK.
3128
+
3129
+ :param child_rule_id: The child rule id.
3130
+ :param remove_parant_expiration: If true, removes the expiration of the parent rule.
3131
+ :param session: The Database session
3132
+ """
3133
+
3134
+ session.flush()
3135
+
3136
+ stmt = select(
3137
+ models.ReplicationRule
3138
+ ).with_hint(
3139
+ models.ReplicationRule, 'INDEX(RULES RULES_CHILD_RULE_ID_IDX)', 'oracle'
3140
+ ).where(
3141
+ models.ReplicationRule.child_rule_id == child_rule_id
3142
+ )
3143
+ parent_rules = session.execute(stmt).scalars().all()
3144
+ for rule in parent_rules:
3145
+ if remove_parent_expiration:
3146
+ rule.expires_at = None
3147
+ rule.child_rule_id = None
3148
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
3149
+
3150
+
3151
+ @stream_session
3152
+ def list_rules_for_rse_decommissioning(
3153
+ rse_id: str,
3154
+ *,
3155
+ session: "Session"
3156
+ ) -> 'Iterator[dict[str, Any]]':
3157
+ """Return a generator of rules at the RSE that is being decommissioned.
3158
+
3159
+ Decommissioning of an RSE involves deleting or moving away all rules that are
3160
+ locking the replicas that exist at the RSE. The rules can be enforcing
3161
+ dataset-level and/or file-level locks. Because rules are defined in terms of
3162
+ RSE expressions, we need to first identify the locks with the RSE id and make
3163
+ the list of rules that are enforcing such locks.
3164
+ This function has two yield statements corresponding to the two types
3165
+ (dataset-level and file-level) of locks. To avoid listing duplicates, the
3166
+ rules identified through the dataset-level locks are excluded from the
3167
+ second query using file-level locks.
3168
+
3169
+ :param rse_id: Id of the RSE being decommissioned.
3170
+ :param session: The database session in use.
3171
+ :returns: A generator that yields rule dictionaries.
3172
+ """
3173
+ # Get rules with dataset locks first.
3174
+ query_rules_from_dataset_locks = select(
3175
+ models.ReplicationRule
3176
+ ).distinct(
3177
+ ).join_from(
3178
+ models.DatasetLock,
3179
+ models.ReplicationRule,
3180
+ models.DatasetLock.rule_id == models.ReplicationRule.id
3181
+ ).where(
3182
+ models.DatasetLock.rse_id == rse_id
3183
+ )
3184
+
3185
+ for rule in session.execute(query_rules_from_dataset_locks).yield_per(5).scalars():
3186
+ yield rule.to_dict()
3187
+
3188
+ # Make a subquery from the previous query to be excluded from the next query
3189
+ dataset_rule_ids = query_rules_from_dataset_locks.with_only_columns(models.ReplicationRule.id)
3190
+
3191
+ # ReplicaLock ("locks") table is not indexed by RSE ID, so we instead go
3192
+ # through the RSEFileAssociation ("replicas") table.
3193
+ query_rules_from_replicas = select(
3194
+ models.ReplicationRule
3195
+ ).prefix_with(
3196
+ '/*+ USE_NL(locks) LEADING(replicas locks) */',
3197
+ dialect='oracle'
3198
+ ).distinct(
3199
+ ).join_from(
3200
+ models.RSEFileAssociation,
3201
+ models.ReplicaLock,
3202
+ and_(models.RSEFileAssociation.scope == models.ReplicaLock.scope,
3203
+ models.RSEFileAssociation.name == models.ReplicaLock.name,
3204
+ models.RSEFileAssociation.rse_id == models.ReplicaLock.rse_id)
3205
+ ).join(
3206
+ models.ReplicationRule,
3207
+ models.ReplicaLock.rule_id == models.ReplicationRule.id
3208
+ ).where(
3209
+ models.RSEFileAssociation.rse_id == rse_id,
3210
+ models.ReplicaLock.rule_id.not_in(dataset_rule_ids)
3211
+ )
3212
+
3213
+ for rule in session.execute(query_rules_from_replicas).yield_per(5).scalars():
3214
+ yield rule.to_dict()
3215
+
3216
+
3217
+ @transactional_session
3218
+ def __find_missing_locks_and_create_them(
3219
+ datasetfiles: 'Sequence[dict[str, Any]]',
3220
+ locks: dict[tuple[InternalScope, str], 'Sequence[models.ReplicaLock]'],
3221
+ replicas: dict[tuple[InternalScope, str], 'Sequence[models.CollectionReplica]'],
3222
+ source_replicas: dict[tuple[InternalScope, str], 'Sequence[models.CollectionReplica]'],
3223
+ rseselector: RSESelector,
3224
+ rule: models.ReplicationRule,
3225
+ source_rses: 'Sequence[str]',
3226
+ *,
3227
+ session: "Session",
3228
+ logger: LoggerFunction = logging.log
3229
+ ) -> None:
3230
+ """
3231
+ Find missing locks for a rule and create them.
3232
+
3233
+ :param datasetfiles: Sequence of dicts holding all datasets and files.
3234
+ :param locks: Dict holding locks.
3235
+ :param replicas: Dict holding replicas.
3236
+ :param source_replicas: Dict holding source replicas.
3237
+ :param rseselector: The RSESelector to be used.
3238
+ :param rule: The rule.
3239
+ :param source_rses: RSE ids for eligible source RSEs.
3240
+ :param session: Session of the db.
3241
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
3242
+ :raises: InsufficientAccountLimit, IntegrityError, InsufficientTargetRSEs
3243
+ :attention: This method modifies the contents of the locks and replicas input parameters.
3244
+ """
3245
+
3246
+ logger(logging.DEBUG, "Finding missing locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
3247
+
3248
+ mod_datasetfiles = [] # List of Datasets and their files in the Tree [{'scope':, 'name':, 'files': []}]
3249
+ # Files are in the format [{'scope':, 'name':, 'bytes':, 'md5':, 'adler32':}]
3250
+
3251
+ for dataset in datasetfiles:
3252
+ mod_files = []
3253
+ preferred_rse_ids = []
3254
+ for file in dataset['files']:
3255
+ if len([lock for lock in locks[(file['scope'], file['name'])] if lock.rule_id == rule.id]) < rule.copies:
3256
+ mod_files.append(file)
3257
+ else:
3258
+ preferred_rse_ids = [lock.rse_id for lock in locks[(file['scope'], file['name'])] if lock.rule_id == rule.id]
3259
+ if mod_files:
3260
+ logger(logging.DEBUG, 'Found missing locks for rule %s, creating them now', str(rule.id))
3261
+ mod_datasetfiles.append({'scope': dataset['scope'], 'name': dataset['name'], 'files': mod_files})
3262
+ __create_locks_replicas_transfers(datasetfiles=mod_datasetfiles,
3263
+ locks=locks,
3264
+ replicas=replicas,
3265
+ source_replicas=source_replicas,
3266
+ rseselector=rseselector,
3267
+ rule=rule,
3268
+ preferred_rse_ids=preferred_rse_ids,
3269
+ source_rses=source_rses,
3270
+ session=session)
3271
+
3272
+ logger(logging.DEBUG, "Finished finding missing locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
3273
+
3274
+
3275
+ @transactional_session
3276
+ def __find_surplus_locks_and_remove_them(
3277
+ datasetfiles: 'Sequence[dict[str, Any]]',
3278
+ locks: dict[tuple[InternalScope, str], list[models.ReplicaLock]],
3279
+ rule: models.ReplicationRule,
3280
+ *,
3281
+ session: "Session",
3282
+ logger: LoggerFunction = logging.log
3283
+ ) -> None:
3284
+ """
3285
+ Find surplocks locks for a rule and delete them.
3286
+
3287
+ :param datasetfiles: Dict holding all datasets and files.
3288
+ :param locks: Dict holding locks.
3289
+ :param rule: The rule.
3290
+ :param session: Session of the db.
3291
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
3292
+ :raises: InsufficientAccountLimit, IntegrityError, InsufficientTargetRSEs
3293
+ :attention: This method modifies the contents of the locks and replicas input parameters.
3294
+ """
3295
+
3296
+ logger(logging.DEBUG, "Finding surplus locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
3297
+
3298
+ account_counter_decreases = {} # {'rse_id': [file_size, file_size, file_size]}
3299
+
3300
+ # Put all the files in one dictionary
3301
+ files = {}
3302
+ for ds in datasetfiles:
3303
+ for file in ds['files']:
3304
+ files[(file['scope'], file['name'])] = True
3305
+
3306
+ for key in locks:
3307
+ if key not in files:
3308
+ # The lock needs to be removed
3309
+ for lock in deepcopy(locks[key]):
3310
+ if lock.rule_id == rule.id:
3311
+ __delete_lock_and_update_replica(lock=lock, purge_replicas=rule.purge_replicas, nowait=True, session=session)
3312
+ if lock.rse_id not in account_counter_decreases:
3313
+ account_counter_decreases[lock.rse_id] = []
3314
+ account_counter_decreases[lock.rse_id].append(lock.bytes)
3315
+ if lock.state == LockState.OK:
3316
+ rule.locks_ok_cnt -= 1
3317
+ elif lock.state == LockState.REPLICATING:
3318
+ rule.locks_replicating_cnt -= 1
3319
+ elif lock.state == LockState.STUCK:
3320
+ rule.locks_stuck_cnt -= 1
3321
+ locks[key].remove(lock)
3322
+
3323
+ logger(logging.DEBUG, "Finished finding surplus locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
3324
+
3325
+
3326
+ @transactional_session
3327
+ def __find_stuck_locks_and_repair_them(
3328
+ datasetfiles: 'Sequence[dict[str, Any]]',
3329
+ locks: dict[tuple[InternalScope, str], 'Sequence[models.ReplicaLock]'],
3330
+ replicas: dict[tuple[InternalScope, str], 'Sequence[models.CollectionReplica]'],
3331
+ source_replicas: dict[tuple[InternalScope, str], 'Sequence[models.CollectionReplica]'],
3332
+ rseselector: RSESelector,
3333
+ rule: models.ReplicationRule,
3334
+ source_rses: 'Sequence[str]',
3335
+ *,
3336
+ session: "Session",
3337
+ logger: LoggerFunction = logging.log
3338
+ ) -> None:
3339
+ """
3340
+ Find stuck locks for a rule and repair them.
3341
+
3342
+ :param datasetfiles: Dict holding all datasets and files.
3343
+ :param locks: Dict holding locks.
3344
+ :param replicas: Dict holding replicas.
3345
+ :param source_replicas: Dict holding source replicas.
3346
+ :param rseselector: The RSESelector to be used.
3347
+ :param rule: The rule.
3348
+ :param source_rses: RSE ids of eligible source RSEs.
3349
+ :param session: Session of the db.
3350
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
3351
+ :raises: InsufficientAccountLimit, IntegrityError, InsufficientTargetRSEs
3352
+ :attention: This method modifies the contents of the locks and replicas input parameters.
3353
+ """
3354
+
3355
+ logger(logging.DEBUG, "Finding and repairing stuck locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
3356
+
3357
+ replicas_to_create, locks_to_create, transfers_to_create, \
3358
+ locks_to_delete = repair_stuck_locks_and_apply_rule_grouping(datasetfiles=datasetfiles,
3359
+ locks=locks,
3360
+ replicas=replicas,
3361
+ source_replicas=source_replicas,
3362
+ rseselector=rseselector,
3363
+ rule=rule,
3364
+ source_rses=source_rses,
3365
+ session=session)
3366
+ # Add the replicas
3367
+ session.add_all([item for sublist in replicas_to_create.values() for item in sublist])
3368
+ session.flush()
3369
+
3370
+ # Add the locks
3371
+ session.add_all([item for sublist in locks_to_create.values() for item in sublist])
3372
+ session.flush()
3373
+
3374
+ # Increase rse_counters
3375
+ for rse_id in replicas_to_create.keys():
3376
+ rse_counter.increase(rse_id=rse_id, files=len(replicas_to_create[rse_id]), bytes_=sum([replica.bytes for replica in replicas_to_create[rse_id]]), session=session)
3377
+
3378
+ # Increase account_counters
3379
+ for rse_id in locks_to_create.keys():
3380
+ account_counter.increase(rse_id=rse_id, account=rule.account, files=len(locks_to_create[rse_id]), bytes_=sum([lock.bytes for lock in locks_to_create[rse_id]]), session=session)
3381
+
3382
+ # Decrease account_counters
3383
+ for rse_id in locks_to_delete:
3384
+ account_counter.decrease(rse_id=rse_id, account=rule.account, files=len(locks_to_delete[rse_id]), bytes_=sum([lock.bytes for lock in locks_to_delete[rse_id]]), session=session)
3385
+
3386
+ # Delete the locks:
3387
+ for lock in [item for sublist in locks_to_delete.values() for item in sublist]:
3388
+ session.delete(lock)
3389
+
3390
+ # Add the transfers
3391
+ request_core.queue_requests(requests=transfers_to_create, session=session)
3392
+ session.flush()
3393
+ logger(logging.DEBUG, "Finished finding and repairing stuck locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
3394
+
3395
+
3396
+ @transactional_session
3397
+ def __evaluate_did_detach(
3398
+ eval_did: models.DataIdentifier,
3399
+ *,
3400
+ session: "Session",
3401
+ logger: LoggerFunction = logging.log
3402
+ ) -> None:
3403
+ """
3404
+ Evaluate a parent did which has children removed.
3405
+
3406
+ :param eval_did: The did object in use.
3407
+ :param session: The database session in use.
3408
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
3409
+ """
3410
+
3411
+ logger(logging.INFO, "Re-Evaluating did %s:%s for DETACH", eval_did.scope, eval_did.name)
3412
+ force_epoch = config_get('rules', 'force_epoch_when_detach', default=False, session=session)
3413
+
3414
+ with METRICS.timer('evaluate_did_detach.total'):
3415
+ # Get all parent DID's
3416
+ parent_dids = rucio.core.did.list_all_parent_dids(scope=eval_did.scope, name=eval_did.name, session=session)
3417
+
3418
+ # Get all RR from parents and eval_did
3419
+ stmt = select(
3420
+ models.ReplicationRule
3421
+ ).where(
3422
+ and_(models.ReplicationRule.scope == eval_did.scope,
3423
+ models.ReplicationRule.name == eval_did.name)
3424
+ ).with_for_update(
3425
+ nowait=True
3426
+ )
3427
+ rules = list(session.execute(stmt).scalars().all())
3428
+ for did in parent_dids:
3429
+ stmt = select(
3430
+ models.ReplicationRule
3431
+ ).where(
3432
+ and_(models.ReplicationRule.scope == did['scope'],
3433
+ models.ReplicationRule.name == did['name'])
3434
+ ).with_for_update(
3435
+ nowait=True
3436
+ )
3437
+ rules.extend(session.execute(stmt).scalars().all())
3438
+
3439
+ # Iterate rules and delete locks
3440
+ transfers_to_delete = [] # [{'scope': , 'name':, 'rse_id':}]
3441
+ account_counter_decreases = {} # {'rse_id': [file_size, file_size, file_size]}
3442
+ for rule in rules:
3443
+ # Get all the files covering this rule
3444
+ files = {}
3445
+ for file in rucio.core.did.list_files(scope=rule.scope, name=rule.name, session=session):
3446
+ files[(file['scope'], file['name'])] = True
3447
+ logger(logging.DEBUG, "Removing locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
3448
+ rule_locks_ok_cnt_before = rule.locks_ok_cnt
3449
+ stmt = select(
3450
+ models.ReplicaLock
3451
+ ).where(
3452
+ models.ReplicaLock.rule_id == rule.id
3453
+ )
3454
+ for lock in session.execute(stmt).scalars().all():
3455
+ if (lock.scope, lock.name) not in files:
3456
+ if __delete_lock_and_update_replica(lock=lock, purge_replicas=force_epoch or rule.purge_replicas, nowait=True, session=session):
3457
+ transfers_to_delete.append({'scope': lock.scope, 'name': lock.name, 'rse_id': lock.rse_id})
3458
+ if lock.rse_id not in account_counter_decreases:
3459
+ account_counter_decreases[lock.rse_id] = []
3460
+ account_counter_decreases[lock.rse_id].append(lock.bytes)
3461
+ if lock.state == LockState.OK:
3462
+ rule.locks_ok_cnt -= 1
3463
+ elif lock.state == LockState.REPLICATING:
3464
+ rule.locks_replicating_cnt -= 1
3465
+ elif lock.state == LockState.STUCK:
3466
+ rule.locks_stuck_cnt -= 1
3467
+ logger(logging.DEBUG, "Finished removing locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
3468
+
3469
+ if eval_did.did_type == DIDType.CONTAINER:
3470
+ # Get all datasets of eval_did
3471
+ child_datasets = {}
3472
+ for ds in rucio.core.did.list_child_datasets(scope=rule.scope, name=rule.name, session=session):
3473
+ child_datasets[(ds['scope'], ds['name'])] = True
3474
+ logger(logging.DEBUG, "Removing dataset_locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
3475
+ stmt = select(
3476
+ models.DatasetLock
3477
+ ).where(
3478
+ models.DatasetLock.rule_id == rule.id
3479
+ )
3480
+ query = session.execute(stmt).scalars().all()
3481
+ for ds_lock in query:
3482
+ if (ds_lock.scope, ds_lock.name) not in child_datasets:
3483
+ ds_lock.delete(flush=False, session=session)
3484
+ logger(logging.DEBUG, "Finished removing dataset_locks for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
3485
+
3486
+ if rule.state == RuleState.SUSPENDED:
3487
+ pass
3488
+ elif rule.state == RuleState.STUCK:
3489
+ pass
3490
+ elif rule.locks_replicating_cnt == 0 and rule.locks_stuck_cnt == 0:
3491
+ rule.state = RuleState.OK
3492
+ if rule.grouping != RuleGrouping.NONE:
3493
+ stmt = update(
3494
+ models.DatasetLock
3495
+ ).where(
3496
+ models.DatasetLock.rule_id == rule.id
3497
+ ).values({
3498
+ models.DatasetLock.state: LockState.OK
3499
+ })
3500
+ session.execute(stmt)
3501
+ session.flush()
3502
+ if rule_locks_ok_cnt_before != rule.locks_ok_cnt:
3503
+ generate_rule_notifications(rule=rule, session=session)
3504
+ generate_email_for_rule_ok_notification(rule=rule, session=session)
3505
+ # Try to release potential parent rules
3506
+ release_parent_rule(child_rule_id=rule.id, session=session)
3507
+
3508
+ # Insert rule history
3509
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
3510
+
3511
+ session.flush()
3512
+
3513
+ # Decrease account_counters
3514
+ for rse_id in account_counter_decreases:
3515
+ account_counter.decrease(rse_id=rse_id, account=rule.account, files=len(account_counter_decreases[rse_id]), bytes_=sum(account_counter_decreases[rse_id]), session=session)
3516
+
3517
+ for transfer in transfers_to_delete:
3518
+ transfers_to_cancel = request_core.cancel_request_did(scope=transfer['scope'], name=transfer['name'], dest_rse_id=transfer['rse_id'], session=session)
3519
+ transfer_core.cancel_transfers(transfers_to_cancel)
3520
+
3521
+
3522
+ @transactional_session
3523
+ def __oldest_file_under(
3524
+ scope: InternalScope,
3525
+ name: str,
3526
+ *,
3527
+ session: "Session"
3528
+ ) -> Optional[tuple[InternalScope, str]]:
3529
+ """
3530
+ Finds oldest file in oldest container/dataset in the container or the dataset, recursively.
3531
+ Oldest means attached to its parent first.
3532
+
3533
+ :param scope: dataset or container scope
3534
+ :param name: dataset or container name
3535
+ :returns: tuple (scope, name) or None
3536
+ """
3537
+ stmt = select(
3538
+ models.DataIdentifierAssociation
3539
+ ).where(
3540
+ and_(models.DataIdentifierAssociation.scope == scope,
3541
+ models.DataIdentifierAssociation.name == name)
3542
+ ).order_by(
3543
+ models.DataIdentifierAssociation.created_at
3544
+ )
3545
+ children = session.execute(stmt).scalars().all()
3546
+ for child in children:
3547
+ if child.child_type == DIDType.FILE:
3548
+ return child.child_scope, child.child_name
3549
+ elif child.child_type in (DIDType.DATASET, DIDType.CONTAINER):
3550
+ out = __oldest_file_under(child.child_scope, child.child_name, session=session)
3551
+ if out:
3552
+ return out
3553
+ return None
3554
+
3555
+
3556
+ @transactional_session
3557
+ def __evaluate_did_attach(
3558
+ eval_did: models.DataIdentifier,
3559
+ *,
3560
+ session: "Session",
3561
+ logger: LoggerFunction = logging.log
3562
+ ) -> None:
3563
+ """
3564
+ Evaluate a parent did which has new children
3565
+
3566
+ :param eval_did: The did object in use.
3567
+ :param session: The database session in use.
3568
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
3569
+ :raises: ReplicationRuleCreationTemporaryFailed
3570
+ """
3571
+
3572
+ logger(logging.INFO, "Re-Evaluating did %s:%s for ATTACH", eval_did.scope, eval_did.name)
3573
+
3574
+ with METRICS.timer('evaluate_did_attach.total'):
3575
+ # Get all parent DID's
3576
+ with METRICS.timer('evaluate_did_attach.list_parent_dids'):
3577
+ parent_dids = rucio.core.did.list_all_parent_dids(scope=eval_did.scope, name=eval_did.name, session=session)
3578
+
3579
+ # Get immediate new child DID's
3580
+ with METRICS.timer('evaluate_did_attach.list_new_child_dids'):
3581
+ stmt = select(
3582
+ models.DataIdentifierAssociation
3583
+ ).with_hint(
3584
+ models.DataIdentifierAssociation, 'INDEX_RS_ASC(CONTENTS CONTENTS_PK)', 'oracle'
3585
+ ).where(
3586
+ and_(models.DataIdentifierAssociation.scope == eval_did.scope,
3587
+ models.DataIdentifierAssociation.name == eval_did.name,
3588
+ models.DataIdentifierAssociation.rule_evaluation == true())
3589
+ )
3590
+ new_child_dids = session.execute(stmt).scalars().all()
3591
+ if new_child_dids:
3592
+ # Get all unsuspended RR from parents and eval_did
3593
+ with METRICS.timer('evaluate_did_attach.get_rules'):
3594
+ rule_clauses = []
3595
+ for did in parent_dids:
3596
+ rule_clauses.append(and_(models.ReplicationRule.scope == did['scope'],
3597
+ models.ReplicationRule.name == did['name']))
3598
+ rule_clauses.append(and_(models.ReplicationRule.scope == eval_did.scope,
3599
+ models.ReplicationRule.name == eval_did.name))
3600
+ stmt = select(
3601
+ models.ReplicationRule
3602
+ ).where(
3603
+ and_(or_(*rule_clauses),
3604
+ models.ReplicationRule.state.not_in([RuleState.SUSPENDED,
3605
+ RuleState.WAITING_APPROVAL,
3606
+ RuleState.INJECT]))
3607
+ ).with_for_update(
3608
+ nowait=True
3609
+ )
3610
+ rules = session.execute(stmt).scalars().all()
3611
+ if rules:
3612
+ # Resolve the new_child_dids to its locks
3613
+ with METRICS.timer('evaluate_did_attach.resolve_did_to_locks_and_replicas'):
3614
+ # Resolve the rules to possible target rses:
3615
+ possible_rses = []
3616
+ source_rses = []
3617
+ for rule in rules:
3618
+ try:
3619
+ vo = rule.account.vo
3620
+ if rule.source_replica_expression:
3621
+ source_rses.extend(parse_expression(rule.source_replica_expression, filter_={'vo': vo}, session=session))
3622
+
3623
+ # if rule.ignore_availability:
3624
+ possible_rses.extend(parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session))
3625
+ # else:
3626
+ # possible_rses.extend(parse_expression(rule.rse_expression, filter={'availability_write': True}, session=session))
3627
+ except (InvalidRSEExpression, RSEWriteBlocked):
3628
+ possible_rses = []
3629
+ break
3630
+
3631
+ source_rses = list(set([rse['id'] for rse in source_rses]))
3632
+ possible_rses = list(set([rse['id'] for rse in possible_rses]))
3633
+
3634
+ datasetfiles, locks, replicas, source_replicas = __resolve_dids_to_locks_and_replicas(dids=new_child_dids,
3635
+ nowait=True,
3636
+ restrict_rses=possible_rses,
3637
+ source_rses=source_rses,
3638
+ session=session)
3639
+
3640
+ # Evaluate the replication rules
3641
+ with METRICS.timer('evaluate_did_attach.evaluate_rules'):
3642
+ for rule in rules:
3643
+ rule_locks_ok_cnt_before = rule.locks_ok_cnt
3644
+
3645
+ # 1. Resolve the rse_expression into a list of RSE-ids
3646
+ try:
3647
+ vo = rule.account.vo
3648
+ if rule.ignore_availability:
3649
+ rses = parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session)
3650
+ else:
3651
+ rses = parse_expression(rule.rse_expression, filter_={'vo': vo, 'availability_write': True}, session=session)
3652
+ source_rses = []
3653
+ if rule.source_replica_expression:
3654
+ source_rses = parse_expression(rule.source_replica_expression, filter_={'vo': vo}, session=session)
3655
+ except (InvalidRSEExpression, RSEWriteBlocked) as error:
3656
+ rule.state = RuleState.STUCK
3657
+ rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
3658
+ rule.save(session=session)
3659
+ # Insert rule history
3660
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
3661
+ # Try to update the DatasetLocks
3662
+ if rule.grouping != RuleGrouping.NONE:
3663
+ stmt = update(
3664
+ models.DatasetLock
3665
+ ).where(
3666
+ models.DatasetLock.rule_id == rule.id
3667
+ ).values({
3668
+ models.DatasetLock.state: LockState.STUCK
3669
+ })
3670
+ session.execute(stmt)
3671
+ continue
3672
+
3673
+ # 2. Create the RSE Selector
3674
+ try:
3675
+ rseselector = RSESelector(account=rule.account,
3676
+ rses=rses,
3677
+ weight=rule.weight,
3678
+ copies=rule.copies,
3679
+ ignore_account_limit=rule.ignore_account_limit,
3680
+ session=session)
3681
+ except (InvalidRuleWeight, InsufficientTargetRSEs, InsufficientAccountLimit, RSEOverQuota) as error:
3682
+ rule.state = RuleState.STUCK
3683
+ rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
3684
+ rule.save(session=session)
3685
+ # Insert rule history
3686
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
3687
+ # Try to update the DatasetLocks
3688
+ if rule.grouping != RuleGrouping.NONE:
3689
+ stmt = update(
3690
+ models.DatasetLock
3691
+ ).where(
3692
+ models.DatasetLock.rule_id == rule.id
3693
+ ).values({
3694
+ models.DatasetLock.state: LockState.STUCK
3695
+ })
3696
+ session.execute(stmt)
3697
+ continue
3698
+
3699
+ # 3. Apply the Replication rule to the Files
3700
+ preferred_rse_ids = []
3701
+ brother_scope_name = None
3702
+
3703
+ if rule.grouping == RuleGrouping.ALL:
3704
+ # get oldest file, recursively, in the rule owner
3705
+ brother_scope_name = __oldest_file_under(rule["scope"], rule["name"], session=session)
3706
+
3707
+ elif rule.grouping == RuleGrouping.DATASET and new_child_dids[0].child_type == DIDType.FILE:
3708
+ # get oldest file in the dataset being evaluated
3709
+ brother_scope_name = __oldest_file_under(eval_did.scope, eval_did.name, session=session)
3710
+
3711
+ if brother_scope_name:
3712
+ scope, name = brother_scope_name
3713
+ file_locks = rucio.core.lock.get_replica_locks(scope=scope, name=name, nowait=True, session=session)
3714
+ preferred_rse_ids = [
3715
+ lock['rse_id']
3716
+ for lock in file_locks
3717
+ if lock['rse_id'] in [rse['id'] for rse in rses] and lock['rule_id'] == rule.id
3718
+ ]
3719
+
3720
+ locks_stuck_before = rule.locks_stuck_cnt
3721
+ try:
3722
+ __create_locks_replicas_transfers(datasetfiles=datasetfiles,
3723
+ locks=locks,
3724
+ replicas=replicas,
3725
+ source_replicas=source_replicas,
3726
+ rseselector=rseselector,
3727
+ rule=rule,
3728
+ preferred_rse_ids=preferred_rse_ids,
3729
+ source_rses=[rse['id'] for rse in source_rses],
3730
+ session=session)
3731
+ except (InsufficientAccountLimit, InsufficientTargetRSEs, RSEOverQuota) as error:
3732
+ rule.state = RuleState.STUCK
3733
+ rule.error = (str(error)[:245] + '...') if len(str(error)) > 245 else str(error)
3734
+ rule.save(session=session)
3735
+ # Insert rule history
3736
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
3737
+ # Try to update the DatasetLocks
3738
+ if rule.grouping != RuleGrouping.NONE:
3739
+ stmt = update(
3740
+ models.DatasetLock
3741
+ ).where(
3742
+ models.DatasetLock.rule_id == rule.id
3743
+ ).values({
3744
+ models.DatasetLock.state: LockState.STUCK
3745
+ })
3746
+ session.execute(stmt)
3747
+ continue
3748
+
3749
+ # 4. Update the Rule State
3750
+ if rule.state == RuleState.STUCK:
3751
+ pass
3752
+ elif rule.locks_stuck_cnt > 0:
3753
+ if locks_stuck_before != rule.locks_stuck_cnt:
3754
+ rule.state = RuleState.STUCK
3755
+ rule.error = 'MissingSourceReplica'
3756
+ stmt = update(
3757
+ models.DatasetLock
3758
+ ).where(
3759
+ models.DatasetLock.rule_id == rule.id
3760
+ ).values({
3761
+ models.DatasetLock.state: LockState.STUCK
3762
+ })
3763
+ session.execute(stmt)
3764
+ elif rule.locks_replicating_cnt > 0:
3765
+ rule.state = RuleState.REPLICATING
3766
+ if rule.grouping != RuleGrouping.NONE:
3767
+ stmt = update(
3768
+ models.DatasetLock
3769
+ ).where(
3770
+ models.DatasetLock.rule_id == rule.id
3771
+ ).values({
3772
+ models.DatasetLock.state: LockState.REPLICATING
3773
+ })
3774
+ session.execute(stmt)
3775
+ elif rule.locks_replicating_cnt == 0 and rule.locks_stuck_cnt == 0:
3776
+ rule.state = RuleState.OK
3777
+ if rule.grouping != RuleGrouping.NONE:
3778
+ stmt = update(
3779
+ models.DatasetLock
3780
+ ).where(
3781
+ models.DatasetLock.rule_id == rule.id
3782
+ ).values({
3783
+ models.DatasetLock.state: LockState.OK
3784
+ })
3785
+ session.execute(stmt)
3786
+ session.flush()
3787
+ if rule_locks_ok_cnt_before < rule.locks_ok_cnt:
3788
+ generate_rule_notifications(rule=rule, session=session)
3789
+ generate_email_for_rule_ok_notification(rule=rule, session=session)
3790
+
3791
+ # Insert rule history
3792
+ insert_rule_history(rule=rule, recent=True, longterm=False, session=session)
3793
+
3794
+ # Unflage the dids
3795
+ with METRICS.timer('evaluate_did_attach.update_did'):
3796
+ for did in new_child_dids:
3797
+ did.rule_evaluation = None
3798
+
3799
+ session.flush()
3800
+
3801
+
3802
+ @transactional_session
3803
+ def __resolve_did_to_locks_and_replicas(
3804
+ did: models.DataIdentifier,
3805
+ nowait: bool = False,
3806
+ restrict_rses: Optional['Sequence[str]'] = None,
3807
+ source_rses: Optional['Sequence[str]'] = None,
3808
+ only_stuck: bool = False,
3809
+ *,
3810
+ session: "Session"
3811
+ ) -> tuple[list[dict[str, Any]],
3812
+ dict[tuple[str, str], models.ReplicaLock],
3813
+ dict[tuple[str, str], models.RSEFileAssociation],
3814
+ dict[tuple[str, str], str]]:
3815
+ """
3816
+ Resolves a did to its constituent children and reads the locks and replicas of all the constituent files.
3817
+
3818
+ :param did: The db object of the did the rule is applied on.
3819
+ :param nowait: Nowait parameter for the FOR UPDATE statement.
3820
+ :param restrict_rses: Possible rses of the rule, so only these replica/locks should be considered.
3821
+ :param source_rses: Source rses for this rule. These replicas are not row-locked.
3822
+ :param only_stuck: Get results only for STUCK locks, if True.
3823
+ :param session: Session of the db.
3824
+ :returns: (datasetfiles, locks, replicas, source_replicas)
3825
+ """
3826
+
3827
+ datasetfiles = [] # List of Datasets and their files in the Tree [{'scope':, 'name':, 'files': []}]
3828
+ # Files are in the format [{'scope':, 'name':, 'bytes':, 'md5':, 'adler32':}]
3829
+ locks = {} # {(scope,name): [SQLAlchemy]}
3830
+ replicas = {} # {(scope, name): [SQLAlchemy]}
3831
+ source_replicas = {} # {(scope, name): [rse_id]
3832
+
3833
+ if did.did_type == DIDType.FILE:
3834
+ datasetfiles = [{'scope': None,
3835
+ 'name': None,
3836
+ 'files': [{'scope': did.scope,
3837
+ 'name': did.name,
3838
+ 'bytes': did.bytes,
3839
+ 'md5': did.md5,
3840
+ 'adler32': did.adler32}]}]
3841
+ locks[(did.scope, did.name)] = rucio.core.lock.get_replica_locks(scope=did.scope, name=did.name, nowait=nowait, restrict_rses=restrict_rses, session=session)
3842
+ replicas[(did.scope, did.name)] = rucio.core.replica.get_and_lock_file_replicas(scope=did.scope, name=did.name, nowait=nowait, restrict_rses=restrict_rses, session=session)
3843
+ if source_rses:
3844
+ source_replicas[(did.scope, did.name)] = rucio.core.replica.get_source_replicas(scope=did.scope, name=did.name, source_rses=source_rses, session=session)
3845
+
3846
+ elif did.did_type == DIDType.DATASET and only_stuck:
3847
+ files = []
3848
+ locks = rucio.core.lock.get_files_and_replica_locks_of_dataset(scope=did.scope, name=did.name, nowait=nowait, restrict_rses=restrict_rses, only_stuck=True, session=session)
3849
+ for file in locks:
3850
+ file_did = rucio.core.did.get_did(scope=file[0], name=file[1], session=session)
3851
+ files.append({'scope': file[0], 'name': file[1], 'bytes': file_did['bytes'], 'md5': file_did['md5'], 'adler32': file_did['adler32']})
3852
+ replicas[(file[0], file[1])] = rucio.core.replica.get_and_lock_file_replicas(scope=file[0], name=file[1], nowait=nowait, restrict_rses=restrict_rses, session=session)
3853
+ if source_rses:
3854
+ source_replicas[(file[0], file[1])] = rucio.core.replica.get_source_replicas(scope=file[0], name=file[1], source_rses=source_rses, session=session)
3855
+ datasetfiles = [{'scope': did.scope,
3856
+ 'name': did.name,
3857
+ 'files': files}]
3858
+
3859
+ elif did.did_type == DIDType.DATASET:
3860
+ files, replicas = rucio.core.replica.get_and_lock_file_replicas_for_dataset(scope=did.scope, name=did.name, nowait=nowait, restrict_rses=restrict_rses, session=session)
3861
+ if source_rses:
3862
+ source_replicas = rucio.core.replica.get_source_replicas_for_dataset(scope=did.scope, name=did.name, source_rses=source_rses, session=session)
3863
+ datasetfiles = [{'scope': did.scope,
3864
+ 'name': did.name,
3865
+ 'files': files}]
3866
+ locks = rucio.core.lock.get_files_and_replica_locks_of_dataset(scope=did.scope, name=did.name, nowait=nowait, restrict_rses=restrict_rses, session=session)
3867
+
3868
+ elif did.did_type == DIDType.CONTAINER and only_stuck:
3869
+
3870
+ for dataset in rucio.core.did.list_child_datasets(scope=did.scope, name=did.name, session=session):
3871
+ files = []
3872
+ tmp_locks = rucio.core.lock.get_files_and_replica_locks_of_dataset(scope=dataset['scope'], name=dataset['name'], nowait=nowait, restrict_rses=restrict_rses, only_stuck=True, session=session)
3873
+ locks = dict(list(locks.items()) + list(tmp_locks.items()))
3874
+ for file in tmp_locks:
3875
+ file_did = rucio.core.did.get_did(scope=file[0], name=file[1], session=session)
3876
+ files.append({'scope': file[0], 'name': file[1], 'bytes': file_did['bytes'], 'md5': file_did['md5'], 'adler32': file_did['adler32']})
3877
+ replicas[(file[0], file[1])] = rucio.core.replica.get_and_lock_file_replicas(scope=file[0], name=file[1], nowait=nowait, restrict_rses=restrict_rses, session=session)
3878
+ if source_rses:
3879
+ source_replicas[(file[0], file[1])] = rucio.core.replica.get_source_replicas(scope=file[0], name=file[1], source_rses=source_rses, session=session)
3880
+ datasetfiles.append({'scope': dataset['scope'],
3881
+ 'name': dataset['name'],
3882
+ 'files': files})
3883
+
3884
+ elif did.did_type == DIDType.CONTAINER:
3885
+
3886
+ for dataset in rucio.core.did.list_child_datasets(scope=did.scope, name=did.name, session=session):
3887
+ files, tmp_replicas = rucio.core.replica.get_and_lock_file_replicas_for_dataset(scope=dataset['scope'], name=dataset['name'], nowait=nowait, restrict_rses=restrict_rses, session=session)
3888
+ if source_rses:
3889
+ tmp_source_replicas = rucio.core.replica.get_source_replicas_for_dataset(scope=dataset['scope'], name=dataset['name'], source_rses=source_rses, session=session)
3890
+ source_replicas = dict(list(source_replicas.items()) + list(tmp_source_replicas.items()))
3891
+ tmp_locks = rucio.core.lock.get_files_and_replica_locks_of_dataset(scope=dataset['scope'], name=dataset['name'], nowait=nowait, restrict_rses=restrict_rses, session=session)
3892
+ datasetfiles.append({'scope': dataset['scope'],
3893
+ 'name': dataset['name'],
3894
+ 'files': files})
3895
+ replicas = dict(list(replicas.items()) + list(tmp_replicas.items()))
3896
+ locks = dict(list(locks.items()) + list(tmp_locks.items()))
3897
+
3898
+ # order datasetfiles for deterministic result
3899
+ try:
3900
+ datasetfiles = sorted(datasetfiles, key=lambda x: "%s%s" % (x['scope'], x['name']))
3901
+ except Exception:
3902
+ pass
3903
+
3904
+ else:
3905
+ raise InvalidReplicationRule('The did \"%s:%s\" has been deleted.' % (did.scope, did.name))
3906
+
3907
+ return datasetfiles, locks, replicas, source_replicas
3908
+
3909
+
3910
+ @transactional_session
3911
+ def __resolve_dids_to_locks_and_replicas(
3912
+ dids: 'Sequence[models.DataIdentifierAssociation]',
3913
+ nowait: bool = False,
3914
+ restrict_rses: Optional['Sequence[str]'] = None,
3915
+ source_rses: Optional['Sequence[str]'] = None,
3916
+ *,
3917
+ session: "Session"
3918
+ ) -> tuple[list[dict[str, Any]],
3919
+ dict[tuple[str, str], models.ReplicaLock],
3920
+ dict[tuple[str, str], models.RSEFileAssociation],
3921
+ dict[tuple[str, str], str]]:
3922
+ """
3923
+ Resolves a list of dids to its constituent children and reads the locks and replicas of all the constituent files.
3924
+
3925
+ :param dids: The list of DataIdentifierAssociation objects.
3926
+ :param nowait: Nowait parameter for the FOR UPDATE statement.
3927
+ :param restrict_rses: Possible rses of the rule, so only these replica/locks should be considered.
3928
+ :param source_rses: Source rses for this rule. These replicas are not row-locked.
3929
+ :param session: Session of the db.
3930
+ :returns: (datasetfiles, locks, replicas, source_replicas)
3931
+ """
3932
+
3933
+ datasetfiles = [] # List of Datasets and their files in the Tree [{'scope':, 'name':, 'files': []}]
3934
+ # Files are in the format [{'scope':, 'name':, 'bytes':, 'md5':, 'adler32':}]
3935
+ locks = {} # {(scope,name): [SQLAlchemy]}
3936
+ replicas = {} # {(scope, name): [SQLAlchemy]}
3937
+ source_replicas = {} # {(scope, name): [rse_id]
3938
+ restrict_rses = restrict_rses or []
3939
+
3940
+ if dids[0].child_type == DIDType.FILE:
3941
+ # All the dids will be files!
3942
+ # Prepare the datasetfiles
3943
+ files = []
3944
+ for did in dids:
3945
+ files.append({'scope': did.child_scope,
3946
+ 'name': did.child_name,
3947
+ 'bytes': did.bytes,
3948
+ 'md5': did.md5,
3949
+ 'adler32': did.adler32})
3950
+ locks[(did.child_scope, did.child_name)] = []
3951
+ replicas[(did.child_scope, did.child_name)] = []
3952
+ source_replicas[(did.child_scope, did.child_name)] = []
3953
+ datasetfiles = [{'scope': dids[0].scope, 'name': dids[0].name, 'files': files}]
3954
+
3955
+ # Prepare the locks and files
3956
+ lock_clauses = []
3957
+ replica_clauses = []
3958
+ for did in dids:
3959
+ lock_clauses.append(and_(models.ReplicaLock.scope == did.child_scope,
3960
+ models.ReplicaLock.name == did.child_name))
3961
+ replica_clauses.append(and_(models.RSEFileAssociation.scope == did.child_scope,
3962
+ models.RSEFileAssociation.name == did.child_name))
3963
+ lock_clause_chunks = [lock_clauses[x:x + 10] for x in range(0, len(lock_clauses), 10)]
3964
+ replica_clause_chunks = [replica_clauses[x:x + 10] for x in range(0, len(replica_clauses), 10)]
3965
+
3966
+ replicas_rse_clause = []
3967
+ source_replicas_rse_clause = []
3968
+ locks_rse_clause = []
3969
+ if restrict_rses:
3970
+ for rse_id in restrict_rses:
3971
+ replicas_rse_clause.append(models.RSEFileAssociation.rse_id == rse_id)
3972
+ locks_rse_clause.append(models.ReplicaLock.rse_id == rse_id)
3973
+ if source_rses:
3974
+ for rse_id in source_rses:
3975
+ source_replicas_rse_clause.append(models.RSEFileAssociation.rse_id == rse_id)
3976
+
3977
+ for lock_clause_chunk in lock_clause_chunks:
3978
+ if locks_rse_clause:
3979
+ stmt = select(
3980
+ models.ReplicaLock
3981
+ ).with_hint(
3982
+ models.ReplicaLock, 'INDEX(LOCKS LOCKS_PK)', 'oracle'
3983
+ ).where(
3984
+ and_(or_(*lock_clause_chunk),
3985
+ or_(*locks_rse_clause))
3986
+ ).with_for_update(
3987
+ nowait=nowait
3988
+ )
3989
+ tmp_locks = session.execute(stmt).scalars().all()
3990
+ else:
3991
+ stmt = select(
3992
+ models.ReplicaLock
3993
+ ).with_hint(
3994
+ models.ReplicaLock, 'INDEX(LOCKS LOCKS_PK)', 'oracle'
3995
+ ).where(
3996
+ or_(*lock_clause_chunk)
3997
+ ).with_for_update(
3998
+ nowait=nowait
3999
+ )
4000
+ tmp_locks = session.execute(stmt).scalars().all()
4001
+ for lock in tmp_locks:
4002
+ if (lock.scope, lock.name) not in locks:
4003
+ locks[(lock.scope, lock.name)] = [lock]
4004
+ else:
4005
+ locks[(lock.scope, lock.name)].append(lock)
4006
+
4007
+ for replica_clause_chunk in replica_clause_chunks:
4008
+ if replicas_rse_clause:
4009
+ stmt = select(
4010
+ models.RSEFileAssociation
4011
+ ).with_hint(
4012
+ models.RSEFileAssociation, 'INDEX(REPLICAS REPLICAS_PK)', 'oracle'
4013
+ ).where(
4014
+ and_(or_(*replica_clause_chunk),
4015
+ or_(*replicas_rse_clause),
4016
+ models.RSEFileAssociation.state != ReplicaState.BEING_DELETED)
4017
+ ).with_for_update(
4018
+ nowait=nowait
4019
+ )
4020
+ tmp_replicas = session.execute(stmt).scalars().all()
4021
+ else:
4022
+ stmt = select(
4023
+ models.RSEFileAssociation
4024
+ ).with_hint(
4025
+ models.RSEFileAssociation, 'INDEX(REPLICAS REPLICAS_PK)', 'oracle'
4026
+ ).where(
4027
+ and_(or_(*replica_clause_chunk),
4028
+ models.RSEFileAssociation.state != ReplicaState.BEING_DELETED)
4029
+ ).with_for_update(
4030
+ nowait=nowait
4031
+ )
4032
+ tmp_replicas = session.execute(stmt).scalars().all()
4033
+ for replica in tmp_replicas:
4034
+ if (replica.scope, replica.name) not in replicas:
4035
+ replicas[(replica.scope, replica.name)] = [replica]
4036
+ else:
4037
+ replicas[(replica.scope, replica.name)].append(replica)
4038
+
4039
+ if source_rses:
4040
+ for replica_clause_chunk in replica_clause_chunks:
4041
+ stmt = select(
4042
+ models.RSEFileAssociation.scope,
4043
+ models.RSEFileAssociation.name,
4044
+ models.RSEFileAssociation.rse_id
4045
+ ).with_hint(
4046
+ models.RSEFileAssociation, 'INDEX(REPLICAS REPLICAS_PK)', 'oracle'
4047
+ ).where(
4048
+ and_(or_(*replica_clause_chunk),
4049
+ or_(*source_replicas_rse_clause),
4050
+ models.RSEFileAssociation.state == ReplicaState.AVAILABLE)
4051
+ )
4052
+ tmp_source_replicas = session.execute(stmt).all()
4053
+ for scope, name, rse_id in tmp_source_replicas:
4054
+ if (scope, name) not in source_replicas:
4055
+ source_replicas[(scope, name)] = [rse_id]
4056
+ else:
4057
+ source_replicas[(scope, name)].append(rse_id)
4058
+ else:
4059
+ # The evaluate_dids will be containers and/or datasets
4060
+ for did in dids:
4061
+ stmt = select(
4062
+ models.DataIdentifier
4063
+ ).where(
4064
+ and_(models.DataIdentifier.scope == did.child_scope,
4065
+ models.DataIdentifier.name == did.child_name)
4066
+ )
4067
+ real_did = session.execute(stmt).scalar_one()
4068
+ tmp_datasetfiles, tmp_locks, tmp_replicas, tmp_source_replicas = __resolve_did_to_locks_and_replicas(did=real_did,
4069
+ nowait=nowait,
4070
+ restrict_rses=restrict_rses,
4071
+ source_rses=source_rses,
4072
+ session=session)
4073
+ datasetfiles.extend(tmp_datasetfiles)
4074
+ locks.update(tmp_locks)
4075
+ replicas.update(tmp_replicas)
4076
+ source_replicas.update(tmp_source_replicas)
4077
+ return datasetfiles, locks, replicas, source_replicas
4078
+
4079
+
4080
+ @transactional_session
4081
+ def __create_locks_replicas_transfers(
4082
+ datasetfiles: 'Sequence[dict[str, Any]]',
4083
+ locks: dict[tuple[InternalScope, str], 'Sequence[models.ReplicaLock]'],
4084
+ replicas: dict[tuple[InternalScope, str], 'Sequence[models.CollectionReplica]'],
4085
+ source_replicas: dict[tuple[InternalScope, str], 'Sequence[models.CollectionReplica]'],
4086
+ rseselector: RSESelector,
4087
+ rule: models.ReplicationRule,
4088
+ preferred_rse_ids: Optional['Sequence[str]'] = None,
4089
+ source_rses: Optional['Sequence[str]'] = None,
4090
+ *,
4091
+ session: "Session",
4092
+ logger: LoggerFunction = logging.log
4093
+ ) -> None:
4094
+ """
4095
+ Apply a created replication rule to a set of files
4096
+
4097
+ :param datasetfiles: Dict holding all datasets and files.
4098
+ :param locks: Dict holding locks.
4099
+ :param replicas: Dict holding replicas.
4100
+ :param source_replicas: Dict holding source replicas.
4101
+ :param rseselector: The RSESelector to be used.
4102
+ :param rule: The rule.
4103
+ :param preferred_rse_ids: Preferred RSE's to select.
4104
+ :param source_rses: RSE ids of eligible source replicas.
4105
+ :param session: Session of the db.
4106
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
4107
+ :raises: InsufficientAccountLimit, IntegrityError, InsufficientTargetRSEs, RSEOverQuota
4108
+ :attention: This method modifies the contents of the locks and replicas input parameters.
4109
+ """
4110
+
4111
+ preferred_rse_ids = preferred_rse_ids or []
4112
+ source_rses = source_rses or []
4113
+ logger(logging.DEBUG, "Creating locks and replicas for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
4114
+
4115
+ replicas_to_create, locks_to_create, transfers_to_create = apply_rule_grouping(datasetfiles=datasetfiles,
4116
+ locks=locks,
4117
+ replicas=replicas,
4118
+ source_replicas=source_replicas,
4119
+ rseselector=rseselector,
4120
+ rule=rule,
4121
+ preferred_rse_ids=preferred_rse_ids,
4122
+ source_rses=source_rses,
4123
+ session=session)
4124
+ # Add the replicas
4125
+ session.add_all([item for sublist in replicas_to_create.values() for item in sublist])
4126
+ session.flush()
4127
+
4128
+ # Add the locks
4129
+ session.add_all([item for sublist in locks_to_create.values() for item in sublist])
4130
+ session.flush()
4131
+
4132
+ # Increase rse_counters
4133
+ for rse_id in replicas_to_create.keys():
4134
+ rse_counter.increase(rse_id=rse_id, files=len(replicas_to_create[rse_id]), bytes_=sum([replica.bytes for replica in replicas_to_create[rse_id]]), session=session)
4135
+
4136
+ # Increase account_counters
4137
+ for rse_id in locks_to_create.keys():
4138
+ account_counter.increase(rse_id=rse_id, account=rule.account, files=len(locks_to_create[rse_id]), bytes_=sum([lock.bytes for lock in locks_to_create[rse_id]]), session=session)
4139
+
4140
+ # Add the transfers
4141
+ logger(logging.DEBUG, "Rule %s [%d/%d/%d] queued %d transfers", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt, len(transfers_to_create))
4142
+ request_core.queue_requests(requests=transfers_to_create, session=session)
4143
+ session.flush()
4144
+ logger(logging.DEBUG, "Finished creating locks and replicas for rule %s [%d/%d/%d]", str(rule.id), rule.locks_ok_cnt, rule.locks_replicating_cnt, rule.locks_stuck_cnt)
4145
+
4146
+
4147
+ @transactional_session
4148
+ def __delete_lock_and_update_replica(
4149
+ lock: models.ReplicaLock,
4150
+ purge_replicas: bool = False,
4151
+ nowait: bool = False,
4152
+ *,
4153
+ session: "Session",
4154
+ logger: LoggerFunction = logging.log
4155
+ ) -> bool:
4156
+ """
4157
+ Delete a lock and update the associated replica.
4158
+
4159
+ :param lock: SQLAlchemy lock object.
4160
+ :param purge_replicas: Purge setting of the rule.
4161
+ :param nowait: The nowait option of the FOR UPDATE statement.
4162
+ :param session: The database session in use.
4163
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
4164
+ :returns: True, if the lock was replicating and the associated transfer should be canceled; False otherwise.
4165
+ """
4166
+
4167
+ logger(logging.DEBUG, "Deleting lock %s:%s for rule %s", lock.scope, lock.name, str(lock.rule_id))
4168
+ lock.delete(session=session, flush=False)
4169
+ try:
4170
+ stmt = select(
4171
+ models.RSEFileAssociation
4172
+ ).where(
4173
+ and_(models.RSEFileAssociation.scope == lock.scope,
4174
+ models.RSEFileAssociation.name == lock.name,
4175
+ models.RSEFileAssociation.rse_id == lock.rse_id)
4176
+ ).with_for_update(
4177
+ nowait=nowait
4178
+ )
4179
+ replica = session.execute(stmt).scalar_one()
4180
+ replica.lock_cnt -= 1
4181
+ if replica.lock_cnt == 0:
4182
+ if purge_replicas:
4183
+ replica.tombstone = OBSOLETE
4184
+ elif replica.state == ReplicaState.UNAVAILABLE:
4185
+ replica.tombstone = OBSOLETE
4186
+ elif replica.accessed_at is not None:
4187
+ replica.tombstone = replica.accessed_at
4188
+ else:
4189
+ replica.tombstone = replica.created_at
4190
+ if lock.state == LockState.REPLICATING:
4191
+ replica.state = ReplicaState.UNAVAILABLE
4192
+ replica.tombstone = OBSOLETE
4193
+ return True
4194
+ except NoResultFound:
4195
+ logger(logging.ERROR, "Replica for lock %s:%s for rule %s on rse %s could not be found", lock.scope, lock.name, str(lock.rule_id), get_rse_name(rse_id=lock.rse_id, session=session))
4196
+ return False
4197
+
4198
+
4199
+ @transactional_session
4200
+ def __create_rule_approval_email(
4201
+ rule: models.ReplicationRule,
4202
+ *,
4203
+ session: "Session"
4204
+ ) -> None:
4205
+ """
4206
+ Create the rule notification email.
4207
+
4208
+ :param rule: The rule object.
4209
+ :param session: The database session in use.
4210
+ """
4211
+
4212
+ filepath = path.join(
4213
+ config_get('common', 'mailtemplatedir'), 'rule_approval_request.tmpl' # type: ignore
4214
+ )
4215
+ with open(filepath, 'r') as templatefile:
4216
+ template = Template(templatefile.read())
4217
+
4218
+ did = rucio.core.did.get_did(
4219
+ scope=rule.scope,
4220
+ name=rule.name,
4221
+ dynamic_depth=DIDType.FILE,
4222
+ session=session
4223
+ )
4224
+
4225
+ reps = rucio.core.replica.list_dataset_replicas(
4226
+ scope=rule.scope, name=rule.name, session=session
4227
+ )
4228
+ rses = [rep['rse_id'] for rep in reps if rep['state'] == ReplicaState.AVAILABLE]
4229
+
4230
+ # RSE occupancy
4231
+ vo = rule.account.vo
4232
+ target_rses = parse_expression(rule.rse_expression, filter_={'vo': vo}, session=session)
4233
+ if len(target_rses) > 1:
4234
+ target_rse = 'Multiple'
4235
+ free_space = 'undefined'
4236
+ free_space_after = 'undefined'
4237
+ else:
4238
+ target_rse = target_rses[0]['rse']
4239
+ target_rse_id = target_rses[0]['id']
4240
+ free_space = 'undefined'
4241
+ free_space_after = 'undefined'
4242
+
4243
+ try:
4244
+ for usage in get_rse_usage(rse_id=target_rse_id, session=session):
4245
+ if usage['source'] == 'storage':
4246
+ free_space = sizefmt(usage['free'])
4247
+ if did['bytes'] is None:
4248
+ free_space_after = 'undefined'
4249
+ else:
4250
+ free_space_after = sizefmt(usage['free'] - did['bytes'])
4251
+ except Exception:
4252
+ pass
4253
+
4254
+ # Resolve recipients:
4255
+ recipients = _create_recipients_list(rse_expression=rule.rse_expression, filter_={'vo': vo}, session=session)
4256
+
4257
+ for recipient in recipients:
4258
+ text = template.safe_substitute(
4259
+ {
4260
+ 'rule_id': str(rule.id),
4261
+ 'created_at': str(rule.created_at),
4262
+ 'expires_at': str(rule.expires_at),
4263
+ 'account': rule.account.external,
4264
+ 'email': get_account(account=rule.account, session=session).email,
4265
+ 'rse_expression': rule.rse_expression,
4266
+ 'comment': rule.comments,
4267
+ 'scope': rule.scope.external,
4268
+ 'name': rule.name,
4269
+ 'did_type': rule.did_type,
4270
+ 'length': '0' if did['length'] is None else str(did['length']),
4271
+ 'bytes': '0' if did['bytes'] is None else sizefmt(did['bytes']),
4272
+ 'open': did.get('open', 'Not Applicable'),
4273
+ 'complete_rses': ', '.join(rses),
4274
+ 'approvers': ','.join([r[0] for r in recipients]),
4275
+ 'approver': recipient[1],
4276
+ 'target_rse': target_rse,
4277
+ 'free_space': free_space,
4278
+ 'free_space_after': free_space_after
4279
+ }
4280
+ )
4281
+
4282
+ add_message(event_type='email',
4283
+ payload={'body': text,
4284
+ 'to': [recipient[0]],
4285
+ 'subject': '[RUCIO] Request to approve replication rule %s' % (str(rule.id))},
4286
+ session=session)
4287
+
4288
+
4289
+ @transactional_session
4290
+ def _create_recipients_list(
4291
+ rse_expression: str,
4292
+ filter_: Optional[str] = None,
4293
+ *,
4294
+ session: "Session"
4295
+ ) -> list[tuple[str, Union[str, InternalAccount]]]:
4296
+ """
4297
+ Create a list of recipients for a notification email based on rse_expression.
4298
+
4299
+ :param rse_expression: The rse_expression.
4300
+ :param session: The database session in use.
4301
+ """
4302
+
4303
+ recipients: list[tuple] = [] # (eMail, account)
4304
+
4305
+ # APPROVERS-LIST
4306
+ # If there are accounts in the approvers-list of any of the RSEs only these should be used
4307
+ for rse in parse_expression(rse_expression, filter_=filter_, session=session):
4308
+ rse_attr = list_rse_attributes(rse_id=rse['id'], session=session)
4309
+ if rse_attr.get(RseAttr.RULE_APPROVERS):
4310
+ for account in rse_attr.get(RseAttr.RULE_APPROVERS).split(','):
4311
+ account = InternalAccount(account)
4312
+ try:
4313
+ email = get_account(account=account, session=session).email
4314
+ if email:
4315
+ recipients.append((email, account))
4316
+ except Exception:
4317
+ pass
4318
+
4319
+ # LOCALGROUPDISK/LOCALGROUPTAPE
4320
+ if not recipients:
4321
+ for rse in parse_expression(rse_expression, filter_=filter_, session=session):
4322
+ rse_attr = list_rse_attributes(rse_id=rse['id'], session=session)
4323
+ if rse_attr.get(RseAttr.TYPE, '') in ('LOCALGROUPDISK', 'LOCALGROUPTAPE'):
4324
+
4325
+ query = select(
4326
+ models.AccountAttrAssociation.account
4327
+ ).where(
4328
+ models.AccountAttrAssociation.key == f'country-{rse_attr.get(RseAttr.COUNTRY, "")}',
4329
+ models.AccountAttrAssociation.value == 'admin'
4330
+ )
4331
+
4332
+ for account in session.execute(query).scalars().all():
4333
+ try:
4334
+ email = get_account(account=account, session=session).email
4335
+ if email:
4336
+ recipients.append((email, account))
4337
+ except Exception:
4338
+ pass
4339
+
4340
+ # GROUPDISK
4341
+ if not recipients:
4342
+ for rse in parse_expression(rse_expression, filter_=filter_, session=session):
4343
+ rse_attr = list_rse_attributes(rse_id=rse['id'], session=session)
4344
+ if rse_attr.get(RseAttr.TYPE, '') == 'GROUPDISK':
4345
+
4346
+ query = select(
4347
+ models.AccountAttrAssociation.account
4348
+ ).where(
4349
+ models.AccountAttrAssociation.key == f'group-{rse_attr.get(RseAttr.PHYSGROUP, "")}',
4350
+ models.AccountAttrAssociation.value == 'admin'
4351
+ )
4352
+
4353
+ for account in session.execute(query).scalars().all():
4354
+ try:
4355
+ email = get_account(account=account, session=session).email
4356
+ if email:
4357
+ recipients.append((email, account))
4358
+ except Exception:
4359
+ pass
4360
+
4361
+ # DDMADMIN as default
4362
+ if not recipients:
4363
+ default_mail_from = config_get(
4364
+ 'core', 'default_mail_from', raise_exception=False, default=None
4365
+ )
4366
+ if default_mail_from:
4367
+ recipients = [(default_mail_from, 'ddmadmin')]
4368
+
4369
+ return list(set(recipients))
4370
+
4371
+
4372
+ def __progress_class(replicating_locks, total_locks):
4373
+ """
4374
+ Returns the progress class (10%, 20%, ...) of currently replicating locks.
4375
+
4376
+ :param replicating_locks: Currently replicating locks.
4377
+ :param total_locks: Total locks.
4378
+ """
4379
+
4380
+ try:
4381
+ return int(float(total_locks - replicating_locks) / float(total_locks) * 10) * 10
4382
+ except Exception:
4383
+ return 0
4384
+
4385
+
4386
+ @policy_filter
4387
+ @transactional_session
4388
+ def archive_localgroupdisk_datasets(
4389
+ scope: InternalScope,
4390
+ name: str,
4391
+ *,
4392
+ session: "Session",
4393
+ logger: LoggerFunction = logging.log
4394
+ ) -> None:
4395
+ """
4396
+ ATLAS policy to archive a dataset which has a replica on LOCALGROUPDISK
4397
+
4398
+ :param scope: Scope of the dataset.
4399
+ :param name: Name of the dataset.
4400
+ :param session: The database session in use.
4401
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
4402
+ """
4403
+
4404
+ rses_to_rebalance = []
4405
+
4406
+ archive = InternalScope('archive', vo=scope.vo)
4407
+ # Check if the archival dataset already exists
4408
+ try:
4409
+ rucio.core.did.get_did(scope=archive, name=name, session=session)
4410
+ return
4411
+ except DataIdentifierNotFound:
4412
+ pass
4413
+
4414
+ # Check if the dataset has a rule on a LOCALGROUPDISK
4415
+ for lock in rucio.core.lock.get_dataset_locks(scope=scope, name=name, session=session):
4416
+ if 'LOCALGROUPDISK' in lock['rse']:
4417
+ rses_to_rebalance.append({'rse_id': lock['rse_id'], 'rse': lock['rse'], 'account': lock['account']})
4418
+ # Remove duplicates from list
4419
+ rses_to_rebalance = [dict(t) for t in set([tuple(sorted(d.items())) for d in rses_to_rebalance])]
4420
+
4421
+ # There is at least one rule on LOCALGROUPDISK
4422
+ if rses_to_rebalance:
4423
+ content = [x for x in rucio.core.did.list_content(scope=scope, name=name, session=session)]
4424
+ if content:
4425
+ # Create the archival dataset
4426
+ did = rucio.core.did.get_did(scope=scope, name=name, session=session)
4427
+ meta = rucio.core.did.get_metadata(scope=scope, name=name, session=session)
4428
+ new_meta = {k: v for k, v in meta.items() if k in ['project', 'datatype', 'run_number', 'stream_name', 'prod_step', 'version', 'campaign', 'task_id', 'panda_id'] and v is not None}
4429
+ rucio.core.did.add_did(scope=archive,
4430
+ name=name,
4431
+ did_type=DIDType.DATASET,
4432
+ account=did['account'],
4433
+ statuses={},
4434
+ meta=new_meta,
4435
+ rules=[],
4436
+ lifetime=None,
4437
+ dids=[],
4438
+ rse_id=None,
4439
+ session=session)
4440
+ rucio.core.did.attach_dids(scope=archive, name=name, dids=content, account=did['account'], session=session)
4441
+ if not did['open']:
4442
+ rucio.core.did.set_status(scope=archive, name=name, open=False, session=session)
4443
+
4444
+ for rse in rses_to_rebalance:
4445
+ add_rule(dids=[{'scope': archive, 'name': name}],
4446
+ account=rse['account'],
4447
+ copies=1,
4448
+ rse_expression=rse['rse'],
4449
+ grouping='DATASET',
4450
+ weight=None,
4451
+ lifetime=None,
4452
+ locked=False,
4453
+ subscription_id=None,
4454
+ ignore_account_limit=True,
4455
+ ignore_availability=True,
4456
+ session=session)
4457
+ logger(logging.DEBUG, 'Re-Scoped %s:%s', scope, name)
4458
+
4459
+
4460
+ @policy_filter
4461
+ @read_session
4462
+ def get_scratch_policy(
4463
+ account: InternalAccount,
4464
+ rses: 'Sequence[dict[str, Any]]',
4465
+ lifetime: Optional[int],
4466
+ *,
4467
+ session: "Session"
4468
+ ) -> Optional[int]:
4469
+ """
4470
+ ATLAS policy for rules on SCRATCHDISK
4471
+
4472
+ :param account: Account of the rule.
4473
+ :param rses: List of RSEs.
4474
+ :param lifetime: Lifetime.
4475
+ :param session: The database session in use.
4476
+ """
4477
+
4478
+ scratchdisk_lifetime = get_scratchdisk_lifetime()
4479
+ # Check SCRATCHDISK Policy
4480
+ if not has_account_attribute(account=account, key='admin', session=session) and (lifetime is None or lifetime > 60 * 60 * 24 * scratchdisk_lifetime):
4481
+ # Check if one of the rses is a SCRATCHDISK:
4482
+ if [rse for rse in rses if list_rse_attributes(rse_id=rse['id'], session=session).get(RseAttr.TYPE) == 'SCRATCHDISK']:
4483
+ lifetime = 60 * 60 * 24 * scratchdisk_lifetime - 1
4484
+ return lifetime