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