rucio 32.8.6__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 (481) hide show
  1. rucio/__init__.py +18 -0
  2. rucio/alembicrevision.py +16 -0
  3. rucio/api/__init__.py +14 -0
  4. rucio/api/account.py +266 -0
  5. rucio/api/account_limit.py +287 -0
  6. rucio/api/authentication.py +302 -0
  7. rucio/api/config.py +218 -0
  8. rucio/api/credential.py +60 -0
  9. rucio/api/did.py +726 -0
  10. rucio/api/dirac.py +71 -0
  11. rucio/api/exporter.py +60 -0
  12. rucio/api/heartbeat.py +62 -0
  13. rucio/api/identity.py +160 -0
  14. rucio/api/importer.py +46 -0
  15. rucio/api/lifetime_exception.py +95 -0
  16. rucio/api/lock.py +131 -0
  17. rucio/api/meta.py +85 -0
  18. rucio/api/permission.py +72 -0
  19. rucio/api/quarantined_replica.py +69 -0
  20. rucio/api/replica.py +528 -0
  21. rucio/api/request.py +220 -0
  22. rucio/api/rse.py +601 -0
  23. rucio/api/rule.py +335 -0
  24. rucio/api/scope.py +89 -0
  25. rucio/api/subscription.py +255 -0
  26. rucio/api/temporary_did.py +49 -0
  27. rucio/api/vo.py +112 -0
  28. rucio/client/__init__.py +16 -0
  29. rucio/client/accountclient.py +413 -0
  30. rucio/client/accountlimitclient.py +155 -0
  31. rucio/client/baseclient.py +929 -0
  32. rucio/client/client.py +77 -0
  33. rucio/client/configclient.py +113 -0
  34. rucio/client/credentialclient.py +54 -0
  35. rucio/client/didclient.py +691 -0
  36. rucio/client/diracclient.py +48 -0
  37. rucio/client/downloadclient.py +1674 -0
  38. rucio/client/exportclient.py +44 -0
  39. rucio/client/fileclient.py +51 -0
  40. rucio/client/importclient.py +42 -0
  41. rucio/client/lifetimeclient.py +74 -0
  42. rucio/client/lockclient.py +99 -0
  43. rucio/client/metaclient.py +137 -0
  44. rucio/client/pingclient.py +45 -0
  45. rucio/client/replicaclient.py +444 -0
  46. rucio/client/requestclient.py +109 -0
  47. rucio/client/rseclient.py +664 -0
  48. rucio/client/ruleclient.py +287 -0
  49. rucio/client/scopeclient.py +88 -0
  50. rucio/client/subscriptionclient.py +161 -0
  51. rucio/client/touchclient.py +78 -0
  52. rucio/client/uploadclient.py +871 -0
  53. rucio/common/__init__.py +14 -0
  54. rucio/common/cache.py +74 -0
  55. rucio/common/config.py +796 -0
  56. rucio/common/constants.py +92 -0
  57. rucio/common/constraints.py +18 -0
  58. rucio/common/didtype.py +187 -0
  59. rucio/common/dumper/__init__.py +306 -0
  60. rucio/common/dumper/consistency.py +449 -0
  61. rucio/common/dumper/data_models.py +325 -0
  62. rucio/common/dumper/path_parsing.py +65 -0
  63. rucio/common/exception.py +1092 -0
  64. rucio/common/extra.py +37 -0
  65. rucio/common/logging.py +404 -0
  66. rucio/common/pcache.py +1387 -0
  67. rucio/common/policy.py +84 -0
  68. rucio/common/schema/__init__.py +143 -0
  69. rucio/common/schema/atlas.py +411 -0
  70. rucio/common/schema/belleii.py +406 -0
  71. rucio/common/schema/cms.py +478 -0
  72. rucio/common/schema/domatpc.py +399 -0
  73. rucio/common/schema/escape.py +424 -0
  74. rucio/common/schema/generic.py +431 -0
  75. rucio/common/schema/generic_multi_vo.py +410 -0
  76. rucio/common/schema/icecube.py +404 -0
  77. rucio/common/schema/lsst.py +423 -0
  78. rucio/common/stomp_utils.py +160 -0
  79. rucio/common/stopwatch.py +56 -0
  80. rucio/common/test_rucio_server.py +148 -0
  81. rucio/common/types.py +158 -0
  82. rucio/common/utils.py +1946 -0
  83. rucio/core/__init__.py +14 -0
  84. rucio/core/account.py +426 -0
  85. rucio/core/account_counter.py +171 -0
  86. rucio/core/account_limit.py +357 -0
  87. rucio/core/authentication.py +563 -0
  88. rucio/core/config.py +386 -0
  89. rucio/core/credential.py +218 -0
  90. rucio/core/did.py +3102 -0
  91. rucio/core/did_meta_plugins/__init__.py +250 -0
  92. rucio/core/did_meta_plugins/did_column_meta.py +326 -0
  93. rucio/core/did_meta_plugins/did_meta_plugin_interface.py +116 -0
  94. rucio/core/did_meta_plugins/filter_engine.py +573 -0
  95. rucio/core/did_meta_plugins/json_meta.py +215 -0
  96. rucio/core/did_meta_plugins/mongo_meta.py +199 -0
  97. rucio/core/did_meta_plugins/postgres_meta.py +317 -0
  98. rucio/core/dirac.py +208 -0
  99. rucio/core/distance.py +164 -0
  100. rucio/core/exporter.py +59 -0
  101. rucio/core/heartbeat.py +263 -0
  102. rucio/core/identity.py +290 -0
  103. rucio/core/importer.py +248 -0
  104. rucio/core/lifetime_exception.py +377 -0
  105. rucio/core/lock.py +474 -0
  106. rucio/core/message.py +241 -0
  107. rucio/core/meta.py +190 -0
  108. rucio/core/monitor.py +441 -0
  109. rucio/core/naming_convention.py +154 -0
  110. rucio/core/nongrid_trace.py +124 -0
  111. rucio/core/oidc.py +1339 -0
  112. rucio/core/permission/__init__.py +107 -0
  113. rucio/core/permission/atlas.py +1333 -0
  114. rucio/core/permission/belleii.py +1076 -0
  115. rucio/core/permission/cms.py +1166 -0
  116. rucio/core/permission/escape.py +1076 -0
  117. rucio/core/permission/generic.py +1128 -0
  118. rucio/core/permission/generic_multi_vo.py +1148 -0
  119. rucio/core/quarantined_replica.py +190 -0
  120. rucio/core/replica.py +3627 -0
  121. rucio/core/replica_sorter.py +368 -0
  122. rucio/core/request.py +2241 -0
  123. rucio/core/rse.py +1835 -0
  124. rucio/core/rse_counter.py +155 -0
  125. rucio/core/rse_expression_parser.py +460 -0
  126. rucio/core/rse_selector.py +277 -0
  127. rucio/core/rule.py +3419 -0
  128. rucio/core/rule_grouping.py +1473 -0
  129. rucio/core/scope.py +152 -0
  130. rucio/core/subscription.py +316 -0
  131. rucio/core/temporary_did.py +188 -0
  132. rucio/core/topology.py +448 -0
  133. rucio/core/trace.py +361 -0
  134. rucio/core/transfer.py +1233 -0
  135. rucio/core/vo.py +151 -0
  136. rucio/core/volatile_replica.py +123 -0
  137. rucio/daemons/__init__.py +14 -0
  138. rucio/daemons/abacus/__init__.py +14 -0
  139. rucio/daemons/abacus/account.py +106 -0
  140. rucio/daemons/abacus/collection_replica.py +113 -0
  141. rucio/daemons/abacus/rse.py +107 -0
  142. rucio/daemons/atropos/__init__.py +14 -0
  143. rucio/daemons/atropos/atropos.py +243 -0
  144. rucio/daemons/auditor/__init__.py +261 -0
  145. rucio/daemons/auditor/hdfs.py +86 -0
  146. rucio/daemons/auditor/srmdumps.py +284 -0
  147. rucio/daemons/automatix/__init__.py +14 -0
  148. rucio/daemons/automatix/automatix.py +281 -0
  149. rucio/daemons/badreplicas/__init__.py +14 -0
  150. rucio/daemons/badreplicas/minos.py +311 -0
  151. rucio/daemons/badreplicas/minos_temporary_expiration.py +173 -0
  152. rucio/daemons/badreplicas/necromancer.py +200 -0
  153. rucio/daemons/bb8/__init__.py +14 -0
  154. rucio/daemons/bb8/bb8.py +356 -0
  155. rucio/daemons/bb8/common.py +762 -0
  156. rucio/daemons/bb8/nuclei_background_rebalance.py +147 -0
  157. rucio/daemons/bb8/t2_background_rebalance.py +146 -0
  158. rucio/daemons/c3po/__init__.py +14 -0
  159. rucio/daemons/c3po/algorithms/__init__.py +14 -0
  160. rucio/daemons/c3po/algorithms/simple.py +131 -0
  161. rucio/daemons/c3po/algorithms/t2_free_space.py +125 -0
  162. rucio/daemons/c3po/algorithms/t2_free_space_only_pop.py +127 -0
  163. rucio/daemons/c3po/algorithms/t2_free_space_only_pop_with_network.py +279 -0
  164. rucio/daemons/c3po/c3po.py +342 -0
  165. rucio/daemons/c3po/collectors/__init__.py +14 -0
  166. rucio/daemons/c3po/collectors/agis.py +108 -0
  167. rucio/daemons/c3po/collectors/free_space.py +62 -0
  168. rucio/daemons/c3po/collectors/jedi_did.py +48 -0
  169. rucio/daemons/c3po/collectors/mock_did.py +46 -0
  170. rucio/daemons/c3po/collectors/network_metrics.py +63 -0
  171. rucio/daemons/c3po/collectors/workload.py +110 -0
  172. rucio/daemons/c3po/utils/__init__.py +14 -0
  173. rucio/daemons/c3po/utils/dataset_cache.py +40 -0
  174. rucio/daemons/c3po/utils/expiring_dataset_cache.py +45 -0
  175. rucio/daemons/c3po/utils/expiring_list.py +63 -0
  176. rucio/daemons/c3po/utils/popularity.py +82 -0
  177. rucio/daemons/c3po/utils/timeseries.py +76 -0
  178. rucio/daemons/cache/__init__.py +14 -0
  179. rucio/daemons/cache/consumer.py +191 -0
  180. rucio/daemons/common.py +391 -0
  181. rucio/daemons/conveyor/__init__.py +14 -0
  182. rucio/daemons/conveyor/common.py +530 -0
  183. rucio/daemons/conveyor/finisher.py +492 -0
  184. rucio/daemons/conveyor/poller.py +372 -0
  185. rucio/daemons/conveyor/preparer.py +198 -0
  186. rucio/daemons/conveyor/receiver.py +206 -0
  187. rucio/daemons/conveyor/stager.py +127 -0
  188. rucio/daemons/conveyor/submitter.py +379 -0
  189. rucio/daemons/conveyor/throttler.py +468 -0
  190. rucio/daemons/follower/__init__.py +14 -0
  191. rucio/daemons/follower/follower.py +97 -0
  192. rucio/daemons/hermes/__init__.py +14 -0
  193. rucio/daemons/hermes/hermes.py +738 -0
  194. rucio/daemons/judge/__init__.py +14 -0
  195. rucio/daemons/judge/cleaner.py +149 -0
  196. rucio/daemons/judge/evaluator.py +172 -0
  197. rucio/daemons/judge/injector.py +154 -0
  198. rucio/daemons/judge/repairer.py +144 -0
  199. rucio/daemons/oauthmanager/__init__.py +14 -0
  200. rucio/daemons/oauthmanager/oauthmanager.py +199 -0
  201. rucio/daemons/reaper/__init__.py +14 -0
  202. rucio/daemons/reaper/dark_reaper.py +272 -0
  203. rucio/daemons/reaper/light_reaper.py +255 -0
  204. rucio/daemons/reaper/reaper.py +701 -0
  205. rucio/daemons/replicarecoverer/__init__.py +14 -0
  206. rucio/daemons/replicarecoverer/suspicious_replica_recoverer.py +487 -0
  207. rucio/daemons/storage/__init__.py +14 -0
  208. rucio/daemons/storage/consistency/__init__.py +14 -0
  209. rucio/daemons/storage/consistency/actions.py +753 -0
  210. rucio/daemons/tracer/__init__.py +14 -0
  211. rucio/daemons/tracer/kronos.py +513 -0
  212. rucio/daemons/transmogrifier/__init__.py +14 -0
  213. rucio/daemons/transmogrifier/transmogrifier.py +753 -0
  214. rucio/daemons/undertaker/__init__.py +14 -0
  215. rucio/daemons/undertaker/undertaker.py +137 -0
  216. rucio/db/__init__.py +14 -0
  217. rucio/db/sqla/__init__.py +38 -0
  218. rucio/db/sqla/constants.py +192 -0
  219. rucio/db/sqla/migrate_repo/__init__.py +14 -0
  220. rucio/db/sqla/migrate_repo/env.py +111 -0
  221. rucio/db/sqla/migrate_repo/versions/01eaf73ab656_add_new_rule_notification_state_progress.py +71 -0
  222. rucio/db/sqla/migrate_repo/versions/0437a40dbfd1_add_eol_at_in_rules.py +50 -0
  223. rucio/db/sqla/migrate_repo/versions/0f1adb7a599a_create_transfer_hops_table.py +61 -0
  224. rucio/db/sqla/migrate_repo/versions/102efcf145f4_added_stuck_at_column_to_rules.py +46 -0
  225. rucio/db/sqla/migrate_repo/versions/13d4f70c66a9_introduce_transfer_limits.py +93 -0
  226. rucio/db/sqla/migrate_repo/versions/140fef722e91_cleanup_distances_table.py +78 -0
  227. rucio/db/sqla/migrate_repo/versions/14ec5aeb64cf_add_request_external_host.py +46 -0
  228. rucio/db/sqla/migrate_repo/versions/156fb5b5a14_add_request_type_to_requests_idx.py +53 -0
  229. rucio/db/sqla/migrate_repo/versions/1677d4d803c8_split_rse_availability_into_multiple.py +69 -0
  230. rucio/db/sqla/migrate_repo/versions/16a0aca82e12_create_index_on_table_replicas_path.py +42 -0
  231. rucio/db/sqla/migrate_repo/versions/1803333ac20f_adding_provenance_and_phys_group.py +46 -0
  232. rucio/db/sqla/migrate_repo/versions/1a29d6a9504c_add_didtype_chck_to_requests.py +61 -0
  233. rucio/db/sqla/migrate_repo/versions/1a80adff031a_create_index_on_rules_hist_recent.py +42 -0
  234. rucio/db/sqla/migrate_repo/versions/1c45d9730ca6_increase_identity_length.py +141 -0
  235. rucio/db/sqla/migrate_repo/versions/1d1215494e95_add_quarantined_replicas_table.py +75 -0
  236. rucio/db/sqla/migrate_repo/versions/1d96f484df21_asynchronous_rules_and_rule_approval.py +75 -0
  237. rucio/db/sqla/migrate_repo/versions/1f46c5f240ac_add_bytes_column_to_bad_replicas.py +46 -0
  238. rucio/db/sqla/migrate_repo/versions/1fc15ab60d43_add_message_history_table.py +51 -0
  239. rucio/db/sqla/migrate_repo/versions/2190e703eb6e_move_rse_settings_to_rse_attributes.py +135 -0
  240. rucio/db/sqla/migrate_repo/versions/21d6b9dc9961_add_mismatch_scheme_state_to_requests.py +65 -0
  241. rucio/db/sqla/migrate_repo/versions/22cf51430c78_add_availability_column_to_table_rses.py +42 -0
  242. rucio/db/sqla/migrate_repo/versions/22d887e4ec0a_create_sources_table.py +66 -0
  243. rucio/db/sqla/migrate_repo/versions/25821a8a45a3_remove_unique_constraint_on_requests.py +54 -0
  244. rucio/db/sqla/migrate_repo/versions/25fc855625cf_added_unique_constraint_to_rules.py +43 -0
  245. rucio/db/sqla/migrate_repo/versions/269fee20dee9_add_repair_cnt_to_locks.py +46 -0
  246. rucio/db/sqla/migrate_repo/versions/271a46ea6244_add_ignore_availability_column_to_rules.py +47 -0
  247. rucio/db/sqla/migrate_repo/versions/277b5fbb41d3_switch_heartbeats_executable.py +54 -0
  248. rucio/db/sqla/migrate_repo/versions/27e3a68927fb_remove_replicas_tombstone_and_replicas_.py +39 -0
  249. rucio/db/sqla/migrate_repo/versions/2854cd9e168_added_rule_id_column.py +48 -0
  250. rucio/db/sqla/migrate_repo/versions/295289b5a800_processed_by_and__at_in_requests.py +47 -0
  251. rucio/db/sqla/migrate_repo/versions/2962ece31cf4_add_nbaccesses_column_in_the_did_table.py +48 -0
  252. rucio/db/sqla/migrate_repo/versions/2af3291ec4c_added_replicas_history_table.py +59 -0
  253. rucio/db/sqla/migrate_repo/versions/2b69addda658_add_columns_for_third_party_copy_read_.py +47 -0
  254. rucio/db/sqla/migrate_repo/versions/2b8e7bcb4783_add_config_table.py +72 -0
  255. rucio/db/sqla/migrate_repo/versions/2ba5229cb54c_add_submitted_at_to_requests_table.py +46 -0
  256. rucio/db/sqla/migrate_repo/versions/2cbee484dcf9_added_column_volume_to_rse_transfer_.py +45 -0
  257. rucio/db/sqla/migrate_repo/versions/2edee4a83846_add_source_to_requests_and_requests_.py +48 -0
  258. rucio/db/sqla/migrate_repo/versions/2eef46be23d4_change_tokens_pk.py +48 -0
  259. rucio/db/sqla/migrate_repo/versions/2f648fc909f3_index_in_rule_history_on_scope_name.py +42 -0
  260. rucio/db/sqla/migrate_repo/versions/3082b8cef557_add_naming_convention_table_and_closed_.py +69 -0
  261. rucio/db/sqla/migrate_repo/versions/30fa38b6434e_add_index_on_service_column_in_the_message_table.py +46 -0
  262. rucio/db/sqla/migrate_repo/versions/3152492b110b_added_staging_area_column.py +78 -0
  263. rucio/db/sqla/migrate_repo/versions/32c7d2783f7e_create_bad_replicas_table.py +62 -0
  264. rucio/db/sqla/migrate_repo/versions/3345511706b8_replicas_table_pk_definition_is_in_.py +74 -0
  265. rucio/db/sqla/migrate_repo/versions/35ef10d1e11b_change_index_on_table_requests.py +44 -0
  266. rucio/db/sqla/migrate_repo/versions/379a19b5332d_create_rse_limits_table.py +67 -0
  267. rucio/db/sqla/migrate_repo/versions/384b96aa0f60_created_rule_history_tables.py +134 -0
  268. rucio/db/sqla/migrate_repo/versions/3ac1660a1a72_extend_distance_table.py +58 -0
  269. rucio/db/sqla/migrate_repo/versions/3ad36e2268b0_create_collection_replicas_updates_table.py +79 -0
  270. rucio/db/sqla/migrate_repo/versions/3c9df354071b_extend_waiting_request_state.py +61 -0
  271. rucio/db/sqla/migrate_repo/versions/3d9813fab443_add_a_new_state_lost_in_badfilesstatus.py +45 -0
  272. rucio/db/sqla/migrate_repo/versions/40ad39ce3160_add_transferred_at_to_requests_table.py +46 -0
  273. rucio/db/sqla/migrate_repo/versions/4207be2fd914_add_notification_column_to_rules.py +65 -0
  274. rucio/db/sqla/migrate_repo/versions/42db2617c364_create_index_on_requests_external_id.py +42 -0
  275. rucio/db/sqla/migrate_repo/versions/436827b13f82_added_column_activity_to_table_requests.py +46 -0
  276. rucio/db/sqla/migrate_repo/versions/44278720f774_update_requests_typ_sta_upd_idx_index.py +46 -0
  277. rucio/db/sqla/migrate_repo/versions/45378a1e76a8_create_collection_replica_table.py +80 -0
  278. rucio/db/sqla/migrate_repo/versions/469d262be19_removing_created_at_index.py +43 -0
  279. rucio/db/sqla/migrate_repo/versions/4783c1f49cb4_create_distance_table.py +61 -0
  280. rucio/db/sqla/migrate_repo/versions/49a21b4d4357_create_index_on_table_tokens.py +47 -0
  281. rucio/db/sqla/migrate_repo/versions/4a2cbedda8b9_add_source_replica_expression_column_to_.py +46 -0
  282. rucio/db/sqla/migrate_repo/versions/4a7182d9578b_added_bytes_length_accessed_at_columns.py +52 -0
  283. rucio/db/sqla/migrate_repo/versions/4bab9edd01fc_create_index_on_requests_rule_id.py +42 -0
  284. rucio/db/sqla/migrate_repo/versions/4c3a4acfe006_new_attr_account_table.py +65 -0
  285. rucio/db/sqla/migrate_repo/versions/4cf0a2e127d4_adding_transient_metadata.py +46 -0
  286. rucio/db/sqla/migrate_repo/versions/50280c53117c_add_qos_class_to_rse.py +47 -0
  287. rucio/db/sqla/migrate_repo/versions/52153819589c_add_rse_id_to_replicas_table.py +45 -0
  288. rucio/db/sqla/migrate_repo/versions/52fd9f4916fa_added_activity_to_rules.py +46 -0
  289. rucio/db/sqla/migrate_repo/versions/53b479c3cb0f_fix_did_meta_table_missing_updated_at_.py +48 -0
  290. rucio/db/sqla/migrate_repo/versions/5673b4b6e843_add_wfms_metadata_to_rule_tables.py +50 -0
  291. rucio/db/sqla/migrate_repo/versions/575767d9f89_added_source_history_table.py +59 -0
  292. rucio/db/sqla/migrate_repo/versions/58bff7008037_add_started_at_to_requests.py +48 -0
  293. rucio/db/sqla/migrate_repo/versions/58c8b78301ab_rename_callback_to_message.py +108 -0
  294. rucio/db/sqla/migrate_repo/versions/5f139f77382a_added_child_rule_id_column.py +57 -0
  295. rucio/db/sqla/migrate_repo/versions/688ef1840840_adding_did_meta_table.py +51 -0
  296. rucio/db/sqla/migrate_repo/versions/6e572a9bfbf3_add_new_split_container_column_to_rules.py +50 -0
  297. rucio/db/sqla/migrate_repo/versions/70587619328_add_comment_column_for_subscriptions.py +46 -0
  298. rucio/db/sqla/migrate_repo/versions/739064d31565_remove_history_table_pks.py +42 -0
  299. rucio/db/sqla/migrate_repo/versions/7541902bf173_add_didsfollowed_and_followevents_table.py +93 -0
  300. rucio/db/sqla/migrate_repo/versions/7ec22226cdbf_new_replica_state_for_temporary_.py +73 -0
  301. rucio/db/sqla/migrate_repo/versions/810a41685bc1_added_columns_rse_transfer_limits.py +52 -0
  302. rucio/db/sqla/migrate_repo/versions/83f991c63a93_correct_rse_expression_length.py +45 -0
  303. rucio/db/sqla/migrate_repo/versions/8523998e2e76_increase_size_of_extended_attributes_.py +46 -0
  304. rucio/db/sqla/migrate_repo/versions/8ea9122275b1_adding_missing_function_based_indices.py +54 -0
  305. rucio/db/sqla/migrate_repo/versions/90f47792bb76_add_clob_payload_to_messages.py +48 -0
  306. rucio/db/sqla/migrate_repo/versions/914b8f02df38_new_table_for_lifetime_model_exceptions.py +70 -0
  307. rucio/db/sqla/migrate_repo/versions/94a5961ddbf2_add_estimator_columns.py +48 -0
  308. rucio/db/sqla/migrate_repo/versions/9a1b149a2044_add_saml_identity_type.py +95 -0
  309. rucio/db/sqla/migrate_repo/versions/9a45bc4ea66d_add_vp_table.py +55 -0
  310. rucio/db/sqla/migrate_repo/versions/9eb936a81eb1_true_is_true.py +74 -0
  311. rucio/db/sqla/migrate_repo/versions/a118956323f8_added_vo_table_and_vo_col_to_rse.py +78 -0
  312. rucio/db/sqla/migrate_repo/versions/a193a275255c_add_status_column_in_messages.py +49 -0
  313. rucio/db/sqla/migrate_repo/versions/a5f6f6e928a7_1_7_0.py +124 -0
  314. rucio/db/sqla/migrate_repo/versions/a616581ee47_added_columns_to_table_requests.py +60 -0
  315. rucio/db/sqla/migrate_repo/versions/a6eb23955c28_state_idx_non_functional.py +53 -0
  316. rucio/db/sqla/migrate_repo/versions/a74275a1ad30_added_global_quota_table.py +56 -0
  317. rucio/db/sqla/migrate_repo/versions/a93e4e47bda_heartbeats.py +67 -0
  318. rucio/db/sqla/migrate_repo/versions/ae2a56fcc89_added_comment_column_to_rules.py +50 -0
  319. rucio/db/sqla/migrate_repo/versions/b4293a99f344_added_column_identity_to_table_tokens.py +46 -0
  320. rucio/db/sqla/migrate_repo/versions/b7d287de34fd_removal_of_replicastate_source.py +92 -0
  321. rucio/db/sqla/migrate_repo/versions/b818052fa670_add_index_to_quarantined_replicas.py +42 -0
  322. rucio/db/sqla/migrate_repo/versions/b8caac94d7f0_add_comments_column_for_subscriptions_.py +46 -0
  323. rucio/db/sqla/migrate_repo/versions/b96a1c7e1cc4_new_bad_pfns_table_and_bad_replicas_.py +147 -0
  324. rucio/db/sqla/migrate_repo/versions/bb695f45c04_extend_request_state.py +78 -0
  325. rucio/db/sqla/migrate_repo/versions/bc68e9946deb_add_staging_timestamps_to_request.py +53 -0
  326. rucio/db/sqla/migrate_repo/versions/bf3baa1c1474_correct_pk_and_idx_for_history_tables.py +74 -0
  327. rucio/db/sqla/migrate_repo/versions/c0937668555f_add_qos_policy_map_table.py +56 -0
  328. rucio/db/sqla/migrate_repo/versions/c129ccdb2d5_add_lumiblocknr_to_dids.py +46 -0
  329. rucio/db/sqla/migrate_repo/versions/ccdbcd48206e_add_did_type_column_index_on_did_meta_.py +68 -0
  330. rucio/db/sqla/migrate_repo/versions/cebad904c4dd_new_payload_column_for_heartbeats.py +48 -0
  331. rucio/db/sqla/migrate_repo/versions/d1189a09c6e0_oauth2_0_and_jwt_feature_support_adding_.py +149 -0
  332. rucio/db/sqla/migrate_repo/versions/d23453595260_extend_request_state_for_preparer.py +106 -0
  333. rucio/db/sqla/migrate_repo/versions/d6dceb1de2d_added_purge_column_to_rules.py +47 -0
  334. rucio/db/sqla/migrate_repo/versions/d6e2c3b2cf26_remove_third_party_copy_column_from_rse.py +45 -0
  335. rucio/db/sqla/migrate_repo/versions/d91002c5841_new_account_limits_table.py +105 -0
  336. rucio/db/sqla/migrate_repo/versions/e138c364ebd0_extending_columns_for_filter_and_.py +52 -0
  337. rucio/db/sqla/migrate_repo/versions/e59300c8b179_support_for_archive.py +106 -0
  338. rucio/db/sqla/migrate_repo/versions/f1b14a8c2ac1_postgres_use_check_constraints.py +30 -0
  339. rucio/db/sqla/migrate_repo/versions/f41ffe206f37_oracle_global_temporary_tables.py +75 -0
  340. rucio/db/sqla/migrate_repo/versions/f85a2962b021_adding_transfertool_column_to_requests_.py +49 -0
  341. rucio/db/sqla/migrate_repo/versions/fa7a7d78b602_increase_refresh_token_size.py +45 -0
  342. rucio/db/sqla/migrate_repo/versions/fb28a95fe288_add_replicas_rse_id_tombstone_idx.py +38 -0
  343. rucio/db/sqla/migrate_repo/versions/fe1a65b176c9_set_third_party_copy_read_and_write_.py +44 -0
  344. rucio/db/sqla/migrate_repo/versions/fe8ea2fa9788_added_third_party_copy_column_to_rse_.py +46 -0
  345. rucio/db/sqla/models.py +1834 -0
  346. rucio/db/sqla/sautils.py +48 -0
  347. rucio/db/sqla/session.py +470 -0
  348. rucio/db/sqla/types.py +207 -0
  349. rucio/db/sqla/util.py +521 -0
  350. rucio/rse/__init__.py +97 -0
  351. rucio/rse/protocols/__init__.py +14 -0
  352. rucio/rse/protocols/cache.py +123 -0
  353. rucio/rse/protocols/dummy.py +112 -0
  354. rucio/rse/protocols/gfal.py +701 -0
  355. rucio/rse/protocols/globus.py +243 -0
  356. rucio/rse/protocols/gsiftp.py +93 -0
  357. rucio/rse/protocols/http_cache.py +83 -0
  358. rucio/rse/protocols/mock.py +124 -0
  359. rucio/rse/protocols/ngarc.py +210 -0
  360. rucio/rse/protocols/posix.py +251 -0
  361. rucio/rse/protocols/protocol.py +530 -0
  362. rucio/rse/protocols/rclone.py +365 -0
  363. rucio/rse/protocols/rfio.py +137 -0
  364. rucio/rse/protocols/srm.py +339 -0
  365. rucio/rse/protocols/ssh.py +414 -0
  366. rucio/rse/protocols/storm.py +207 -0
  367. rucio/rse/protocols/webdav.py +547 -0
  368. rucio/rse/protocols/xrootd.py +295 -0
  369. rucio/rse/rsemanager.py +752 -0
  370. rucio/tests/__init__.py +14 -0
  371. rucio/tests/common.py +244 -0
  372. rucio/tests/common_server.py +132 -0
  373. rucio/transfertool/__init__.py +14 -0
  374. rucio/transfertool/fts3.py +1484 -0
  375. rucio/transfertool/globus.py +200 -0
  376. rucio/transfertool/globus_library.py +182 -0
  377. rucio/transfertool/mock.py +81 -0
  378. rucio/transfertool/transfertool.py +212 -0
  379. rucio/vcsversion.py +11 -0
  380. rucio/version.py +46 -0
  381. rucio/web/__init__.py +14 -0
  382. rucio/web/rest/__init__.py +14 -0
  383. rucio/web/rest/flaskapi/__init__.py +14 -0
  384. rucio/web/rest/flaskapi/authenticated_bp.py +28 -0
  385. rucio/web/rest/flaskapi/v1/__init__.py +14 -0
  386. rucio/web/rest/flaskapi/v1/accountlimits.py +234 -0
  387. rucio/web/rest/flaskapi/v1/accounts.py +1088 -0
  388. rucio/web/rest/flaskapi/v1/archives.py +100 -0
  389. rucio/web/rest/flaskapi/v1/auth.py +1642 -0
  390. rucio/web/rest/flaskapi/v1/common.py +385 -0
  391. rucio/web/rest/flaskapi/v1/config.py +305 -0
  392. rucio/web/rest/flaskapi/v1/credentials.py +213 -0
  393. rucio/web/rest/flaskapi/v1/dids.py +2204 -0
  394. rucio/web/rest/flaskapi/v1/dirac.py +116 -0
  395. rucio/web/rest/flaskapi/v1/export.py +77 -0
  396. rucio/web/rest/flaskapi/v1/heartbeats.py +129 -0
  397. rucio/web/rest/flaskapi/v1/identities.py +263 -0
  398. rucio/web/rest/flaskapi/v1/import.py +133 -0
  399. rucio/web/rest/flaskapi/v1/lifetime_exceptions.py +315 -0
  400. rucio/web/rest/flaskapi/v1/locks.py +360 -0
  401. rucio/web/rest/flaskapi/v1/main.py +83 -0
  402. rucio/web/rest/flaskapi/v1/meta.py +226 -0
  403. rucio/web/rest/flaskapi/v1/metrics.py +37 -0
  404. rucio/web/rest/flaskapi/v1/nongrid_traces.py +97 -0
  405. rucio/web/rest/flaskapi/v1/ping.py +89 -0
  406. rucio/web/rest/flaskapi/v1/redirect.py +366 -0
  407. rucio/web/rest/flaskapi/v1/replicas.py +1866 -0
  408. rucio/web/rest/flaskapi/v1/requests.py +841 -0
  409. rucio/web/rest/flaskapi/v1/rses.py +2204 -0
  410. rucio/web/rest/flaskapi/v1/rules.py +824 -0
  411. rucio/web/rest/flaskapi/v1/scopes.py +161 -0
  412. rucio/web/rest/flaskapi/v1/subscriptions.py +646 -0
  413. rucio/web/rest/flaskapi/v1/templates/auth_crash.html +80 -0
  414. rucio/web/rest/flaskapi/v1/templates/auth_granted.html +82 -0
  415. rucio/web/rest/flaskapi/v1/tmp_dids.py +115 -0
  416. rucio/web/rest/flaskapi/v1/traces.py +100 -0
  417. rucio/web/rest/flaskapi/v1/vos.py +280 -0
  418. rucio/web/rest/main.py +19 -0
  419. rucio/web/rest/metrics.py +28 -0
  420. rucio-32.8.6.data/data/rucio/etc/alembic.ini.template +71 -0
  421. rucio-32.8.6.data/data/rucio/etc/alembic_offline.ini.template +74 -0
  422. rucio-32.8.6.data/data/rucio/etc/globus-config.yml.template +5 -0
  423. rucio-32.8.6.data/data/rucio/etc/ldap.cfg.template +30 -0
  424. rucio-32.8.6.data/data/rucio/etc/mail_templates/rule_approval_request.tmpl +38 -0
  425. rucio-32.8.6.data/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +4 -0
  426. rucio-32.8.6.data/data/rucio/etc/mail_templates/rule_approved_user.tmpl +17 -0
  427. rucio-32.8.6.data/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +6 -0
  428. rucio-32.8.6.data/data/rucio/etc/mail_templates/rule_denied_user.tmpl +17 -0
  429. rucio-32.8.6.data/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +19 -0
  430. rucio-32.8.6.data/data/rucio/etc/rse-accounts.cfg.template +25 -0
  431. rucio-32.8.6.data/data/rucio/etc/rucio.cfg.atlas.client.template +42 -0
  432. rucio-32.8.6.data/data/rucio/etc/rucio.cfg.template +257 -0
  433. rucio-32.8.6.data/data/rucio/etc/rucio_multi_vo.cfg.template +234 -0
  434. rucio-32.8.6.data/data/rucio/requirements.txt +55 -0
  435. rucio-32.8.6.data/data/rucio/tools/bootstrap.py +34 -0
  436. rucio-32.8.6.data/data/rucio/tools/merge_rucio_configs.py +147 -0
  437. rucio-32.8.6.data/data/rucio/tools/reset_database.py +40 -0
  438. rucio-32.8.6.data/scripts/rucio +2540 -0
  439. rucio-32.8.6.data/scripts/rucio-abacus-account +75 -0
  440. rucio-32.8.6.data/scripts/rucio-abacus-collection-replica +47 -0
  441. rucio-32.8.6.data/scripts/rucio-abacus-rse +79 -0
  442. rucio-32.8.6.data/scripts/rucio-admin +2434 -0
  443. rucio-32.8.6.data/scripts/rucio-atropos +61 -0
  444. rucio-32.8.6.data/scripts/rucio-auditor +199 -0
  445. rucio-32.8.6.data/scripts/rucio-automatix +51 -0
  446. rucio-32.8.6.data/scripts/rucio-bb8 +58 -0
  447. rucio-32.8.6.data/scripts/rucio-c3po +86 -0
  448. rucio-32.8.6.data/scripts/rucio-cache-client +135 -0
  449. rucio-32.8.6.data/scripts/rucio-cache-consumer +43 -0
  450. rucio-32.8.6.data/scripts/rucio-conveyor-finisher +59 -0
  451. rucio-32.8.6.data/scripts/rucio-conveyor-poller +67 -0
  452. rucio-32.8.6.data/scripts/rucio-conveyor-preparer +38 -0
  453. rucio-32.8.6.data/scripts/rucio-conveyor-receiver +44 -0
  454. rucio-32.8.6.data/scripts/rucio-conveyor-stager +77 -0
  455. rucio-32.8.6.data/scripts/rucio-conveyor-submitter +140 -0
  456. rucio-32.8.6.data/scripts/rucio-conveyor-throttler +105 -0
  457. rucio-32.8.6.data/scripts/rucio-dark-reaper +54 -0
  458. rucio-32.8.6.data/scripts/rucio-dumper +159 -0
  459. rucio-32.8.6.data/scripts/rucio-follower +45 -0
  460. rucio-32.8.6.data/scripts/rucio-hermes +55 -0
  461. rucio-32.8.6.data/scripts/rucio-judge-cleaner +90 -0
  462. rucio-32.8.6.data/scripts/rucio-judge-evaluator +138 -0
  463. rucio-32.8.6.data/scripts/rucio-judge-injector +45 -0
  464. rucio-32.8.6.data/scripts/rucio-judge-repairer +45 -0
  465. rucio-32.8.6.data/scripts/rucio-kronos +45 -0
  466. rucio-32.8.6.data/scripts/rucio-light-reaper +53 -0
  467. rucio-32.8.6.data/scripts/rucio-minos +54 -0
  468. rucio-32.8.6.data/scripts/rucio-minos-temporary-expiration +51 -0
  469. rucio-32.8.6.data/scripts/rucio-necromancer +121 -0
  470. rucio-32.8.6.data/scripts/rucio-oauth-manager +64 -0
  471. rucio-32.8.6.data/scripts/rucio-reaper +84 -0
  472. rucio-32.8.6.data/scripts/rucio-replica-recoverer +249 -0
  473. rucio-32.8.6.data/scripts/rucio-storage-consistency-actions +75 -0
  474. rucio-32.8.6.data/scripts/rucio-transmogrifier +78 -0
  475. rucio-32.8.6.data/scripts/rucio-undertaker +77 -0
  476. rucio-32.8.6.dist-info/METADATA +83 -0
  477. rucio-32.8.6.dist-info/RECORD +481 -0
  478. rucio-32.8.6.dist-info/WHEEL +5 -0
  479. rucio-32.8.6.dist-info/licenses/AUTHORS.rst +94 -0
  480. rucio-32.8.6.dist-info/licenses/LICENSE +201 -0
  481. rucio-32.8.6.dist-info/top_level.txt +1 -0
rucio/core/request.py ADDED
@@ -0,0 +1,2241 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright European Organization for Nuclear Research (CERN) since 2012
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import datetime
17
+ import json
18
+ import logging
19
+ import traceback
20
+ import uuid
21
+ from collections import namedtuple
22
+ from collections.abc import Sequence
23
+ from typing import TYPE_CHECKING, Any, Optional, Union
24
+
25
+ from sqlalchemy import and_, or_, update, select, delete, exists, insert
26
+ from sqlalchemy.exc import IntegrityError
27
+ from sqlalchemy.orm import aliased
28
+ from sqlalchemy.sql.expression import asc, true, false, null, func
29
+
30
+ from rucio.common.config import config_get_bool
31
+ from rucio.common.exception import RequestNotFound, RucioException, UnsupportedOperation, InvalidRSEExpression
32
+ from rucio.common.types import InternalAccount, InternalScope
33
+ from rucio.common.utils import generate_uuid, chunks
34
+ from rucio.core.message import add_message, add_messages
35
+ from rucio.core.monitor import MetricManager
36
+ from rucio.core.rse import get_rse_attribute, get_rse_name, get_rse_vo, RseData
37
+ from rucio.core.rse_expression_parser import parse_expression
38
+ from rucio.db.sqla import models, filter_thread_work
39
+ from rucio.db.sqla.constants import RequestState, RequestType, LockState, RequestErrMsg, ReplicaState, TransferLimitDirection
40
+ from rucio.db.sqla.session import read_session, transactional_session, stream_session
41
+ from rucio.db.sqla.util import temp_table_mngr
42
+
43
+ RequestAndState = namedtuple('RequestAndState', ['request_id', 'request_state'])
44
+
45
+ if TYPE_CHECKING:
46
+ from rucio.core.rse import RseCollection
47
+
48
+ from sqlalchemy.orm import Session
49
+
50
+ """
51
+ The core request.py is specifically for handling requests.
52
+ Requests accessed by external_id (So called transfers), are covered in the core transfer.py
53
+ """
54
+
55
+ METRICS = MetricManager(module=__name__)
56
+
57
+
58
+ class RequestSource:
59
+ def __init__(self, rse_data, ranking=None, distance=None, file_path=None, scheme=None, url=None):
60
+ self.rse = rse_data
61
+ self.distance = distance if distance is not None else 9999
62
+ self.ranking = ranking if ranking is not None else 0
63
+ self.file_path = file_path
64
+ self.scheme = scheme
65
+ self.url = url
66
+
67
+ def __str__(self):
68
+ return "src_rse={}".format(self.rse)
69
+
70
+
71
+ class RequestWithSources:
72
+ def __init__(
73
+ self,
74
+ id_: Optional[str],
75
+ request_type: RequestType,
76
+ rule_id: Optional[str],
77
+ scope: InternalScope,
78
+ name: str,
79
+ md5: str,
80
+ adler32: str,
81
+ byte_count: int,
82
+ activity: str,
83
+ attributes: Optional[Union[str, dict[str, Any]]],
84
+ previous_attempt_id: Optional[str],
85
+ dest_rse_data: RseData,
86
+ account: InternalAccount,
87
+ retry_count: int,
88
+ priority: int,
89
+ transfertool: str,
90
+ requested_at: Optional[datetime.datetime] = None,
91
+ ):
92
+
93
+ self.request_id = id_
94
+ self.request_type = request_type
95
+ self.rule_id = rule_id
96
+ self.scope = scope
97
+ self.name = name
98
+ self.md5 = md5
99
+ self.adler32 = adler32
100
+ self.byte_count = byte_count
101
+ self.activity = activity
102
+ self._dict_attributes = None
103
+ self._db_attributes = attributes
104
+ self.previous_attempt_id = previous_attempt_id
105
+ self.dest_rse = dest_rse_data
106
+ self.account = account
107
+ self.retry_count = retry_count or 0
108
+ self.priority = priority if priority is not None else 3
109
+ self.transfertool = transfertool
110
+ self.requested_at = requested_at if requested_at else datetime.datetime.utcnow()
111
+
112
+ self.sources: list[RequestSource] = []
113
+ self.requested_source: Optional[RequestSource] = None
114
+
115
+ def __str__(self):
116
+ return "{}({}:{})".format(self.request_id, self.scope, self.name)
117
+
118
+ @property
119
+ def attributes(self):
120
+ if self._dict_attributes is None:
121
+ self.attributes = self._db_attributes
122
+ return self._dict_attributes
123
+
124
+ @attributes.setter
125
+ def attributes(self, db_attributes):
126
+ attr = {}
127
+ if db_attributes:
128
+ if isinstance(db_attributes, dict):
129
+ attr = json.loads(json.dumps(db_attributes))
130
+ else:
131
+ attr = json.loads(str(db_attributes))
132
+ # parse source expression
133
+ attr['source_replica_expression'] = attr["source_replica_expression"] if (attr and "source_replica_expression" in attr) else None
134
+ attr['allow_tape_source'] = attr["allow_tape_source"] if (attr and "allow_tape_source" in attr) else True
135
+ attr['dsn'] = attr["ds_name"] if (attr and "ds_name" in attr) else None
136
+ attr['lifetime'] = attr.get('lifetime', -1)
137
+ self._dict_attributes = attr
138
+
139
+
140
+ def should_retry_request(req, retry_protocol_mismatches):
141
+ """
142
+ Whether should retry this request.
143
+
144
+ :param request: Request as a dictionary.
145
+ :param retry_protocol_mismatches: Boolean to retry the transfer in case of protocol mismatch.
146
+ :returns: True if should retry it; False if no more retry.
147
+ """
148
+ if is_intermediate_hop(req):
149
+ # This is an intermediate request in a multi-hop transfer. It must not be re-scheduled on its own.
150
+ # If needed, it will be re-scheduled via the creation of a new multi-hop transfer.
151
+ return False
152
+ if req['state'] == RequestState.SUBMITTING:
153
+ return True
154
+ if req['state'] == RequestState.NO_SOURCES or req['state'] == RequestState.ONLY_TAPE_SOURCES:
155
+ return False
156
+ # hardcoded for now - only requeue a couple of times
157
+ if req['retry_count'] is None or req['retry_count'] < 3:
158
+ if req['state'] == RequestState.MISMATCH_SCHEME:
159
+ return retry_protocol_mismatches
160
+ return True
161
+ return False
162
+
163
+
164
+ @METRICS.time_it
165
+ @transactional_session
166
+ def requeue_and_archive(request, source_ranking_update=True, retry_protocol_mismatches=False, *, session: "Session", logger=logging.log):
167
+ """
168
+ Requeue and archive a failed request.
169
+ TODO: Multiple requeue.
170
+
171
+ :param request: Original request.
172
+ :param source_ranking_update Boolean. If True, the source ranking is decreased (making the sources less likely to be used)
173
+ :param session: Database session to use.
174
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
175
+ """
176
+
177
+ # Probably not needed anymore
178
+ request_id = request['request_id']
179
+ new_req = get_request(request_id, session=session)
180
+
181
+ if new_req:
182
+ new_req['sources'] = get_sources(request_id, session=session)
183
+ archive_request(request_id, session=session)
184
+
185
+ if should_retry_request(new_req, retry_protocol_mismatches):
186
+ new_req['request_id'] = generate_uuid()
187
+ new_req['previous_attempt_id'] = request_id
188
+ if new_req['retry_count'] is None:
189
+ new_req['retry_count'] = 1
190
+ elif new_req['state'] != RequestState.SUBMITTING:
191
+ new_req['retry_count'] += 1
192
+
193
+ if source_ranking_update and new_req['sources']:
194
+ for i in range(len(new_req['sources'])):
195
+ if new_req['sources'][i]['is_using']:
196
+ if new_req['sources'][i]['ranking'] is None:
197
+ new_req['sources'][i]['ranking'] = -1
198
+ else:
199
+ new_req['sources'][i]['ranking'] -= 1
200
+ new_req['sources'][i]['is_using'] = False
201
+ new_req.pop('state', None)
202
+ queue_requests([new_req], session=session, logger=logger)
203
+ return new_req
204
+ else:
205
+ raise RequestNotFound
206
+ return None
207
+
208
+
209
+ @METRICS.count_it
210
+ @transactional_session
211
+ def queue_requests(requests, *, session: "Session", logger=logging.log):
212
+ """
213
+ Submit transfer requests on destination RSEs for data identifiers.
214
+
215
+ :param requests: List of dictionaries containing request metadata.
216
+ :param session: Database session to use.
217
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
218
+ :returns: List of Request-IDs as 32 character hex strings.
219
+ """
220
+ logger(logging.DEBUG, "queue requests")
221
+
222
+ request_clause = []
223
+ rses = {}
224
+ preparer_enabled = config_get_bool('conveyor', 'use_preparer', raise_exception=False, default=False)
225
+ for req in requests:
226
+
227
+ if isinstance(req['attributes'], str):
228
+ req['attributes'] = json.loads(req['attributes'] or '{}')
229
+ if isinstance(req['attributes'], str):
230
+ req['attributes'] = json.loads(req['attributes'] or '{}')
231
+
232
+ if req['request_type'] == RequestType.TRANSFER:
233
+ request_clause.append(and_(models.Request.scope == req['scope'],
234
+ models.Request.name == req['name'],
235
+ models.Request.dest_rse_id == req['dest_rse_id'],
236
+ models.Request.request_type == RequestType.TRANSFER))
237
+
238
+ if req['dest_rse_id'] not in rses:
239
+ rses[req['dest_rse_id']] = get_rse_name(req['dest_rse_id'], session=session)
240
+
241
+ # Check existing requests
242
+ existing_requests = []
243
+ if request_clause:
244
+ for requests_condition in chunks(request_clause, 1000):
245
+ stmt = select(
246
+ models.Request.scope,
247
+ models.Request.name,
248
+ models.Request.dest_rse_id
249
+ ).with_hint(
250
+ models.Request, "INDEX(REQUESTS REQUESTS_SC_NA_RS_TY_UQ_IDX)", 'oracle'
251
+ ).where(
252
+ or_(*requests_condition)
253
+ )
254
+ existing_requests.extend(session.execute(stmt))
255
+
256
+ new_requests, sources, messages = [], [], []
257
+ for request in requests:
258
+ dest_rse_name = get_rse_name(rse_id=request['dest_rse_id'], session=session)
259
+ if request['request_type'] == RequestType.TRANSFER and (request['scope'], request['name'], request['dest_rse_id']) in existing_requests:
260
+ logger(logging.WARNING, 'Request TYPE %s for DID %s:%s at RSE %s exists - ignoring' % (request['request_type'],
261
+ request['scope'],
262
+ request['name'],
263
+ dest_rse_name))
264
+ continue
265
+
266
+ def temp_serializer(obj):
267
+ if isinstance(obj, (InternalAccount, InternalScope)):
268
+ return obj.internal
269
+ raise TypeError('Could not serialise object %r' % obj)
270
+
271
+ if 'state' not in request:
272
+ request['state'] = RequestState.PREPARING if preparer_enabled else RequestState.QUEUED
273
+
274
+ new_request = {'request_type': request['request_type'],
275
+ 'scope': request['scope'],
276
+ 'name': request['name'],
277
+ 'dest_rse_id': request['dest_rse_id'],
278
+ 'source_rse_id': request.get('source_rse_id', None),
279
+ 'attributes': json.dumps(request['attributes'], default=temp_serializer),
280
+ 'state': request['state'],
281
+ 'rule_id': request['rule_id'],
282
+ 'activity': request['attributes']['activity'],
283
+ 'bytes': request['attributes']['bytes'],
284
+ 'md5': request['attributes']['md5'],
285
+ 'adler32': request['attributes']['adler32'],
286
+ 'account': request.get('account', None),
287
+ 'priority': request['attributes'].get('priority', None),
288
+ 'requested_at': request.get('requested_at', None),
289
+ 'retry_count': request['retry_count']}
290
+ if 'transfertool' in request:
291
+ new_request['transfertool'] = request['transfertool']
292
+ if 'previous_attempt_id' in request and 'retry_count' in request:
293
+ new_request['previous_attempt_id'] = request['previous_attempt_id']
294
+ new_request['id'] = request['request_id']
295
+ else:
296
+ new_request['id'] = generate_uuid()
297
+ new_requests.append(new_request)
298
+
299
+ if 'sources' in request and request['sources']:
300
+ for source in request['sources']:
301
+ sources.append({'request_id': new_request['id'],
302
+ 'scope': request['scope'],
303
+ 'name': request['name'],
304
+ 'rse_id': source['rse_id'],
305
+ 'dest_rse_id': request['dest_rse_id'],
306
+ 'ranking': source['ranking'],
307
+ 'bytes': source['bytes'],
308
+ 'url': source['url'],
309
+ 'is_using': source['is_using']})
310
+
311
+ if request['request_type']:
312
+ transfer_status = '%s-%s' % (request['request_type'].name, request['state'].name)
313
+ else:
314
+ transfer_status = 'transfer-%s' % request['state'].name
315
+ transfer_status = transfer_status.lower()
316
+
317
+ payload = {'request-id': new_request['id'],
318
+ 'request-type': request['request_type'].name.lower(),
319
+ 'scope': request['scope'].external,
320
+ 'name': request['name'],
321
+ 'dst-rse-id': request['dest_rse_id'],
322
+ 'dst-rse': dest_rse_name,
323
+ 'state': request['state'].name.lower(),
324
+ 'retry-count': request['retry_count'],
325
+ 'rule-id': str(request['rule_id']),
326
+ 'activity': request['attributes']['activity'],
327
+ 'file-size': request['attributes']['bytes'],
328
+ 'bytes': request['attributes']['bytes'],
329
+ 'checksum-md5': request['attributes']['md5'],
330
+ 'checksum-adler': request['attributes']['adler32'],
331
+ 'queued_at': str(datetime.datetime.utcnow())}
332
+
333
+ messages.append({'event_type': transfer_status,
334
+ 'payload': payload})
335
+
336
+ for requests_chunk in chunks(new_requests, 1000):
337
+ session.execute(insert(models.Request), requests_chunk)
338
+
339
+ for sources_chunk in chunks(sources, 1000):
340
+ session.execute(insert(models.Source), sources_chunk)
341
+
342
+ add_messages(messages, session=session)
343
+
344
+ return new_requests
345
+
346
+
347
+ @transactional_session
348
+ def list_and_mark_transfer_requests_and_source_replicas(
349
+ rse_collection: "RseCollection",
350
+ processed_by: Optional[str] = None,
351
+ processed_at_delay: int = 600,
352
+ total_workers: int = 0,
353
+ worker_number: int = 0,
354
+ partition_hash_var: Optional[str] = None,
355
+ limit: Optional[int] = None,
356
+ activity: Optional[str] = None,
357
+ older_than: Optional[datetime.datetime] = None,
358
+ rses: Optional[Sequence[str]] = None,
359
+ request_type: Optional[list[RequestType]] = None,
360
+ request_state: Optional[RequestState] = None,
361
+ required_source_rse_attrs: Optional[list[str]] = None,
362
+ ignore_availability: bool = False,
363
+ transfertool: Optional[str] = None,
364
+ *,
365
+ session: "Session",
366
+ ) -> dict[str, RequestWithSources]:
367
+ """
368
+ List requests with source replicas
369
+ :param rse_collection: the RSE collection being used
370
+ :param processed_by: the daemon/executable running this query
371
+ :param processed_at_delay: how many second to ignore a request if it's already being processed by the same daemon
372
+ :param total_workers: Number of total workers.
373
+ :param worker_number: Id of the executing worker.
374
+ :param partition_hash_var: The hash variable used for partitioning thread work
375
+ :param limit: Integer of requests to retrieve.
376
+ :param activity: Activity to be selected.
377
+ :param older_than: Only select requests older than this DateTime.
378
+ :param rses: List of rse_id to select requests.
379
+ :param request_type: Filter on the given request type.
380
+ :param request_state: Filter on the given request state
381
+ :param transfertool: The transfer tool as specified in rucio.cfg.
382
+ :param required_source_rse_attrs: Only select source RSEs having these attributes set
383
+ :param ignore_availability: Ignore blocklisted RSEs
384
+ :param session: Database session to use.
385
+ :returns: List of RequestWithSources objects.
386
+ """
387
+
388
+ if partition_hash_var is None:
389
+ partition_hash_var = 'requests.id'
390
+
391
+ if request_state is None:
392
+ request_state = RequestState.QUEUED
393
+
394
+ if request_type is None:
395
+ request_type = [RequestType.TRANSFER]
396
+
397
+ sub_requests = select(
398
+ models.Request.id,
399
+ models.Request.request_type,
400
+ models.Request.rule_id,
401
+ models.Request.scope,
402
+ models.Request.name,
403
+ models.Request.md5,
404
+ models.Request.adler32,
405
+ models.Request.bytes,
406
+ models.Request.activity,
407
+ models.Request.attributes,
408
+ models.Request.previous_attempt_id,
409
+ models.Request.source_rse_id,
410
+ models.Request.dest_rse_id,
411
+ models.Request.retry_count,
412
+ models.Request.account,
413
+ models.Request.created_at,
414
+ models.Request.requested_at,
415
+ models.Request.priority,
416
+ models.Request.transfertool
417
+ ).with_hint(
418
+ models.Request, "INDEX(REQUESTS REQUESTS_TYP_STA_UPD_IDX)", 'oracle'
419
+ ).where(
420
+ models.Request.state == request_state,
421
+ models.Request.request_type.in_(request_type)
422
+ ).join(
423
+ models.RSE,
424
+ models.RSE.id == models.Request.dest_rse_id
425
+ ).where(
426
+ models.RSE.deleted == false()
427
+ ).outerjoin(
428
+ models.TransferHop,
429
+ models.TransferHop.next_hop_request_id == models.Request.id
430
+ ).where(
431
+ models.TransferHop.next_hop_request_id == null()
432
+ ).order_by(
433
+ models.Request.created_at
434
+ )
435
+
436
+ if processed_by:
437
+ sub_requests = sub_requests.where(
438
+ or_(
439
+ models.Request.last_processed_by.is_(null()),
440
+ models.Request.last_processed_by != processed_by,
441
+ models.Request.last_processed_at < datetime.datetime.utcnow() - datetime.timedelta(seconds=processed_at_delay)
442
+ )
443
+ )
444
+
445
+ if not ignore_availability:
446
+ sub_requests = sub_requests.where(models.RSE.availability_write == true())
447
+
448
+ if isinstance(older_than, datetime.datetime):
449
+ sub_requests = sub_requests.where(models.Request.requested_at < older_than)
450
+
451
+ if activity:
452
+ sub_requests = sub_requests.where(models.Request.activity == activity)
453
+
454
+ # if a transfertool is specified make sure to filter for those requests and apply related index
455
+ if transfertool:
456
+ sub_requests = sub_requests.where(models.Request.transfertool == transfertool)
457
+ sub_requests = sub_requests.with_hint(models.Request, "INDEX(REQUESTS REQUESTS_TYP_STA_TRA_ACT_IDX)", 'oracle')
458
+ else:
459
+ sub_requests = sub_requests.with_hint(models.Request, "INDEX(REQUESTS REQUESTS_TYP_STA_UPD_IDX)", 'oracle')
460
+
461
+ if rses:
462
+ temp_table_cls = temp_table_mngr(session).create_id_table()
463
+
464
+ session.execute(insert(temp_table_cls), [{'id': rse_id} for rse_id in rses])
465
+
466
+ sub_requests = sub_requests.join(temp_table_cls, temp_table_cls.id == models.RSE.id)
467
+
468
+ sub_requests = filter_thread_work(session=session, query=sub_requests, total_threads=total_workers, thread_id=worker_number, hash_variable=partition_hash_var)
469
+
470
+ if limit:
471
+ sub_requests = sub_requests.limit(limit)
472
+
473
+ sub_requests = sub_requests.subquery()
474
+
475
+ stmt = select(
476
+ sub_requests.c.id,
477
+ sub_requests.c.request_type,
478
+ sub_requests.c.rule_id,
479
+ sub_requests.c.scope,
480
+ sub_requests.c.name,
481
+ sub_requests.c.md5,
482
+ sub_requests.c.adler32,
483
+ sub_requests.c.bytes,
484
+ sub_requests.c.activity,
485
+ sub_requests.c.attributes,
486
+ sub_requests.c.previous_attempt_id,
487
+ sub_requests.c.source_rse_id,
488
+ sub_requests.c.dest_rse_id,
489
+ sub_requests.c.account,
490
+ sub_requests.c.retry_count,
491
+ sub_requests.c.priority,
492
+ sub_requests.c.transfertool,
493
+ sub_requests.c.requested_at,
494
+ models.RSE.id.label("replica_rse_id"),
495
+ models.RSE.rse.label("replica_rse_name"),
496
+ models.RSEFileAssociation.path,
497
+ models.Source.ranking.label("source_ranking"),
498
+ models.Source.url.label("source_url"),
499
+ models.Distance.distance
500
+ ).order_by(
501
+ sub_requests.c.created_at
502
+ ).outerjoin(
503
+ models.RSEFileAssociation,
504
+ and_(sub_requests.c.scope == models.RSEFileAssociation.scope,
505
+ sub_requests.c.name == models.RSEFileAssociation.name,
506
+ models.RSEFileAssociation.state == ReplicaState.AVAILABLE,
507
+ sub_requests.c.dest_rse_id != models.RSEFileAssociation.rse_id)
508
+ ).with_hint(
509
+ models.RSEFileAssociation, "INDEX(REPLICAS REPLICAS_PK)", 'oracle'
510
+ ).outerjoin(
511
+ models.RSE,
512
+ and_(models.RSE.id == models.RSEFileAssociation.rse_id,
513
+ models.RSE.deleted == false())
514
+ ).outerjoin(
515
+ models.Source,
516
+ and_(sub_requests.c.id == models.Source.request_id,
517
+ models.RSE.id == models.Source.rse_id)
518
+ ).with_hint(
519
+ models.Source, "INDEX(SOURCES SOURCES_PK)", 'oracle'
520
+ ).outerjoin(
521
+ models.Distance,
522
+ and_(sub_requests.c.dest_rse_id == models.Distance.dest_rse_id,
523
+ models.RSEFileAssociation.rse_id == models.Distance.src_rse_id)
524
+ ).with_hint(
525
+ models.Distance, "INDEX(DISTANCES DISTANCES_PK)", 'oracle'
526
+ )
527
+
528
+ for attribute in required_source_rse_attrs or ():
529
+ rse_attr_alias = aliased(models.RSEAttrAssociation)
530
+ stmt = stmt.where(
531
+ exists(
532
+ select(
533
+ 1
534
+ ).where(
535
+ rse_attr_alias.rse_id == models.RSE.id,
536
+ rse_attr_alias.key == attribute
537
+ )
538
+ )
539
+ )
540
+
541
+ requests_by_id = {}
542
+ for (request_id, req_type, rule_id, scope, name, md5, adler32, byte_count, activity, attributes, previous_attempt_id, source_rse_id, dest_rse_id, account, retry_count,
543
+ priority, transfertool, requested_at, replica_rse_id, replica_rse_name, file_path, source_ranking, source_url, distance) in session.execute(stmt):
544
+
545
+ request = requests_by_id.get(request_id)
546
+ if not request:
547
+ request = RequestWithSources(id_=request_id, request_type=req_type, rule_id=rule_id, scope=scope, name=name,
548
+ md5=md5, adler32=adler32, byte_count=byte_count, activity=activity, attributes=attributes,
549
+ previous_attempt_id=previous_attempt_id, dest_rse_data=rse_collection[dest_rse_id],
550
+ account=account, retry_count=retry_count, priority=priority, transfertool=transfertool,
551
+ requested_at=requested_at)
552
+ requests_by_id[request_id] = request
553
+ # if STAGEIN and destination RSE is QoS make sure the source is included
554
+ if request.request_type == RequestType.STAGEIN and get_rse_attribute(rse_id=dest_rse_id, key='staging_required', session=session):
555
+ source = RequestSource(rse_data=rse_collection[dest_rse_id])
556
+ request.sources.append(source)
557
+
558
+ if replica_rse_id is not None:
559
+ replica_rse = rse_collection[replica_rse_id]
560
+ replica_rse.name = replica_rse_name
561
+ source = RequestSource(rse_data=replica_rse, file_path=file_path,
562
+ ranking=source_ranking, distance=distance, url=source_url)
563
+ request.sources.append(source)
564
+ if source_rse_id == replica_rse_id:
565
+ request.requested_source = source
566
+
567
+ if processed_by:
568
+ for chunk in chunks(requests_by_id, 100):
569
+ stmt = update(
570
+ models.Request
571
+ ).where(
572
+ models.Request.id.in_(chunk)
573
+ ).execution_options(
574
+ synchronize_session=False
575
+ ).values(
576
+ {
577
+ models.Request.last_processed_by: processed_by,
578
+ models.Request.last_processed_at: datetime.datetime.now(),
579
+ }
580
+ )
581
+ session.execute(stmt)
582
+
583
+ return requests_by_id
584
+
585
+
586
+ @read_session
587
+ def fetch_paths(request_id, *, session: "Session"):
588
+ """
589
+ Find the paths for which the provided request is a constituent hop.
590
+
591
+ Returns a dict: {initial_request_id1: path1, ...}. Each path is an ordered list of request_ids.
592
+ """
593
+ transfer_hop_alias = aliased(models.TransferHop)
594
+ stmt = select(
595
+ models.TransferHop,
596
+ ).join(
597
+ transfer_hop_alias,
598
+ and_(
599
+ transfer_hop_alias.initial_request_id == models.TransferHop.initial_request_id,
600
+ or_(transfer_hop_alias.request_id == request_id,
601
+ transfer_hop_alias.initial_request_id == request_id),
602
+ )
603
+ )
604
+
605
+ parents_by_initial_request = {}
606
+ for hop, in session.execute(stmt):
607
+ parents_by_initial_request.setdefault(hop.initial_request_id, {})[hop.next_hop_request_id] = hop.request_id
608
+
609
+ paths = {}
610
+ for initial_request_id, parents in parents_by_initial_request.items():
611
+ path = []
612
+ cur_request = initial_request_id
613
+ path.append(cur_request)
614
+ while parents.get(cur_request):
615
+ cur_request = parents[cur_request]
616
+ path.append(cur_request)
617
+ paths[initial_request_id] = list(reversed(path))
618
+ return paths
619
+
620
+
621
+ @METRICS.time_it
622
+ @transactional_session
623
+ def get_and_mark_next(
624
+ rse_collection: "RseCollection",
625
+ request_type,
626
+ state,
627
+ processed_by: Optional[str] = None,
628
+ processed_at_delay: int = 600,
629
+ limit: int = 100,
630
+ older_than: "Optional[datetime.datetime]" = None,
631
+ rse_id: "Optional[str]" = None,
632
+ activity: "Optional[str]" = None,
633
+ total_workers: int = 0,
634
+ worker_number: int = 0,
635
+ mode_all=False,
636
+ hash_variable='id',
637
+ activity_shares=None,
638
+ include_dependent=True,
639
+ transfertool=None,
640
+ *,
641
+ session: "Session"
642
+ ):
643
+ """
644
+ Retrieve the next requests matching the request type and state.
645
+ Workers are balanced via hashing to reduce concurrency on database.
646
+
647
+ :param rse_collection: the RSE collection being used
648
+ :param request_type: Type of the request as a string or list of strings.
649
+ :param state: State of the request as a string or list of strings.
650
+ :param processed_by: the daemon/executable running this query
651
+ :param processed_at_delay: how many second to ignore a request if it's already being processed by the same daemon
652
+ :param limit: Integer of requests to retrieve.
653
+ :param older_than: Only select requests older than this DateTime.
654
+ :param rse_id: The RSE to filter on.
655
+ :param activity: The activity to filter on.
656
+ :param total_workers: Number of total workers.
657
+ :param worker_number: Id of the executing worker.
658
+ :param mode_all: If set to True the function returns everything, if set to False returns list of dictionaries {'request_id': x, 'external_host': y, 'external_id': z}.
659
+ :param hash_variable: The variable to use to perform the partitioning. By default it uses the request id.
660
+ :param activity_shares: Activity shares dictionary, with number of requests
661
+ :param include_dependent: If true, includes transfers which have a previous hop dependency on other transfers
662
+ :param transfertool: The transfer tool as specified in rucio.cfg.
663
+ :param session: Database session to use.
664
+ :returns: Request as a dictionary.
665
+ """
666
+ request_type_metric_label = '.'.join(a.name for a in request_type) if isinstance(request_type, list) else request_type.name
667
+ state_metric_label = '.'.join(s.name for s in state) if isinstance(state, list) else state.name
668
+ METRICS.counter('get_next.requests.{request_type}.{state}').labels(request_type=request_type_metric_label, state=state_metric_label).inc()
669
+
670
+ # lists of one element are not allowed by SQLA, so just duplicate the item
671
+ if type(request_type) is not list:
672
+ request_type = [request_type, request_type]
673
+ elif len(request_type) == 1:
674
+ request_type = [request_type[0], request_type[0]]
675
+ if type(state) is not list:
676
+ state = [state, state]
677
+ elif len(state) == 1:
678
+ state = [state[0], state[0]]
679
+
680
+ result = []
681
+ if not activity_shares:
682
+ activity_shares = [None]
683
+
684
+ for share in activity_shares:
685
+
686
+ query = select(
687
+ models.Request.id
688
+ ).where(
689
+ models.Request.state.in_(state),
690
+ models.Request.request_type.in_(request_type)
691
+ ).order_by(
692
+ asc(models.Request.updated_at)
693
+ )
694
+ if processed_by:
695
+ query = query.where(
696
+ or_(
697
+ models.Request.last_processed_by.is_(null()),
698
+ models.Request.last_processed_by != processed_by,
699
+ models.Request.last_processed_at < datetime.datetime.utcnow() - datetime.timedelta(seconds=processed_at_delay)
700
+ )
701
+ )
702
+ if transfertool:
703
+ query = query.with_hint(
704
+ models.Request, "INDEX(REQUESTS REQUESTS_TYP_STA_TRA_ACT_IDX)", 'oracle'
705
+ ).where(
706
+ models.Request.transfertool == transfertool
707
+ )
708
+ else:
709
+ query = query.with_hint(
710
+ models.Request, "INDEX(REQUESTS REQUESTS_TYP_STA_UPD_IDX)", 'oracle'
711
+ )
712
+
713
+ if not include_dependent:
714
+ # filter out transfers which depend on some other "previous hop" requests.
715
+ # In particular, this is used to avoid multiple finishers trying to archive different
716
+ # transfers from the same path and thus having concurrent deletion of same rows from
717
+ # the transfer_hop table.
718
+ query = query.outerjoin(
719
+ models.TransferHop,
720
+ models.TransferHop.next_hop_request_id == models.Request.id
721
+ ).where(
722
+ models.TransferHop.next_hop_request_id == null()
723
+ )
724
+
725
+ if isinstance(older_than, datetime.datetime):
726
+ query = query.filter(models.Request.updated_at < older_than)
727
+
728
+ if rse_id:
729
+ query = query.filter(models.Request.dest_rse_id == rse_id)
730
+
731
+ if share:
732
+ query = query.filter(models.Request.activity == share)
733
+ elif activity:
734
+ query = query.filter(models.Request.activity == activity)
735
+
736
+ query = filter_thread_work(session=session, query=query, total_threads=total_workers, thread_id=worker_number, hash_variable=hash_variable)
737
+
738
+ if share:
739
+ query = query.limit(activity_shares[share])
740
+ else:
741
+ query = query.limit(limit)
742
+
743
+ if session.bind.dialect.name == 'oracle':
744
+ query = select(
745
+ models.Request
746
+ ).where(
747
+ models.Request.id.in_(query)
748
+ ).with_for_update(
749
+ skip_locked=True
750
+ )
751
+ else:
752
+ query = query.with_only_columns(
753
+ models.Request
754
+ ).with_for_update(
755
+ skip_locked=True,
756
+ of=models.Request.last_processed_by
757
+ )
758
+ query_result = session.execute(query).scalars()
759
+ if query_result:
760
+ if mode_all:
761
+ for res in query_result:
762
+ res_dict = res.to_dict()
763
+ res_dict['request_id'] = res_dict['id']
764
+ res_dict['attributes'] = json.loads(str(res_dict['attributes'] or '{}'))
765
+
766
+ dst_id = res_dict['dest_rse_id']
767
+ src_id = res_dict['source_rse_id']
768
+ res_dict['dst_rse'] = rse_collection[dst_id].ensure_loaded(load_name=True)
769
+ res_dict['src_rse'] = rse_collection[src_id].ensure_loaded(load_name=True) if src_id is not None else None
770
+
771
+ result.append(res_dict)
772
+ else:
773
+ for res in query_result:
774
+ result.append({'request_id': res.id, 'external_host': res.external_host, 'external_id': res.external_id})
775
+
776
+ request_ids = {r['request_id'] for r in result}
777
+ if processed_by and request_ids:
778
+ for chunk in chunks(request_ids, 100):
779
+ stmt = update(
780
+ models.Request
781
+ ).where(
782
+ models.Request.id.in_(chunk)
783
+ ).execution_options(
784
+ synchronize_session=False
785
+ ).values(
786
+ {
787
+ models.Request.last_processed_by: processed_by,
788
+ models.Request.last_processed_at: datetime.datetime.now(),
789
+ }
790
+ )
791
+ session.execute(stmt)
792
+
793
+ return result
794
+
795
+
796
+ @transactional_session
797
+ def update_request(
798
+ request_id: str,
799
+ state: Optional[RequestState] = None,
800
+ transferred_at: Optional[datetime.datetime] = None,
801
+ started_at: Optional[datetime.datetime] = None,
802
+ staging_started_at: Optional[datetime.datetime] = None,
803
+ staging_finished_at: Optional[datetime.datetime] = None,
804
+ source_rse_id: Optional[str] = None,
805
+ err_msg: Optional[str] = None,
806
+ attributes: Optional[dict[str, str]] = None,
807
+ priority: Optional[int] = None,
808
+ transfertool: Optional[str] = None,
809
+ *,
810
+ raise_on_missing: bool = False,
811
+ session: "Session",
812
+ ):
813
+
814
+ rowcount = 0
815
+ try:
816
+ update_items: dict[Any, Any] = {
817
+ models.Request.updated_at: datetime.datetime.utcnow()
818
+ }
819
+ if state is not None:
820
+ update_items[models.Request.state] = state
821
+ if transferred_at is not None:
822
+ update_items[models.Request.transferred_at] = transferred_at
823
+ if started_at is not None:
824
+ update_items[models.Request.started_at] = started_at
825
+ if staging_started_at is not None:
826
+ update_items[models.Request.staging_started_at] = staging_started_at
827
+ if staging_finished_at is not None:
828
+ update_items[models.Request.staging_finished_at] = staging_finished_at
829
+ if source_rse_id is not None:
830
+ update_items[models.Request.source_rse_id] = source_rse_id
831
+ if err_msg is not None:
832
+ update_items[models.Request.err_msg] = err_msg
833
+ if attributes is not None:
834
+ update_items[models.Request.attributes] = json.dumps(attributes)
835
+ if priority is not None:
836
+ update_items[models.Request.priority] = priority
837
+ if transfertool is not None:
838
+ update_items[models.Request.transfertool] = transfertool
839
+
840
+ stmt = update(
841
+ models.Request
842
+ ).where(
843
+ models.Request.id == request_id
844
+ ).execution_options(
845
+ synchronize_session=False
846
+ ).values(
847
+ update_items
848
+ )
849
+ rowcount = session.execute(stmt).rowcount
850
+
851
+ except IntegrityError as error:
852
+ raise RucioException(error.args)
853
+
854
+ if not rowcount and raise_on_missing:
855
+ raise UnsupportedOperation("Request %s state cannot be updated." % request_id)
856
+
857
+ if rowcount:
858
+ return True
859
+ return False
860
+
861
+
862
+ @METRICS.count_it
863
+ @transactional_session
864
+ def set_request_state(
865
+ request_id: str,
866
+ state: Optional[RequestState] = None,
867
+ external_id: Optional[str] = None,
868
+ transferred_at: Optional[datetime.datetime] = None,
869
+ started_at: Optional[datetime.datetime] = None,
870
+ staging_started_at: Optional[datetime.datetime] = None,
871
+ staging_finished_at: Optional[datetime.datetime] = None,
872
+ source_rse_id: Optional[str] = None,
873
+ err_msg: Optional[str] = None,
874
+ attributes: Optional[dict[str, str]] = None,
875
+ *,
876
+ session: "Session",
877
+ logger=logging.log
878
+ ):
879
+ """
880
+ Update the state of a request.
881
+
882
+ :param request_id: Request-ID as a 32 character hex string.
883
+ :param state: New state as string.
884
+ :param external_id: External transfer job id as a string.
885
+ :param transferred_at: Transferred at timestamp
886
+ :param started_at: Started at timestamp
887
+ :param staging_started_at: Timestamp indicating the moment the stage beggins
888
+ :param staging_finished_at: Timestamp indicating the moment the stage ends
889
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
890
+ :param session: Database session to use.
891
+ """
892
+
893
+ # TODO: Should this be a private method?
894
+
895
+ request = get_request(request_id, session=session)
896
+ if not request:
897
+ # The request was deleted in the meantime. Ignore it.
898
+ logger(logging.WARNING, "Request %s not found. Cannot set its state to %s", request_id, state)
899
+ return
900
+
901
+ if state in [RequestState.FAILED, RequestState.DONE, RequestState.LOST] and (request["external_id"] != external_id):
902
+ logger(logging.ERROR, "Request %s should not be updated to 'Failed' or 'Done' without external transfer_id" % request_id)
903
+ else:
904
+ update_request(
905
+ request_id=request_id,
906
+ state=state,
907
+ transferred_at=transferred_at,
908
+ started_at=started_at,
909
+ staging_started_at=staging_started_at,
910
+ staging_finished_at=staging_finished_at,
911
+ source_rse_id=source_rse_id,
912
+ err_msg=err_msg,
913
+ attributes=attributes,
914
+ raise_on_missing=True,
915
+ session=session,
916
+ )
917
+
918
+
919
+ @METRICS.count_it
920
+ @transactional_session
921
+ def set_requests_state_if_possible(request_ids, new_state, *, session: "Session", logger=logging.log):
922
+ """
923
+ Bulk update the state of requests. Skips silently if the request_id does not exist.
924
+
925
+ :param request_ids: List of (Request-ID as a 32 character hex string).
926
+ :param new_state: New state as string.
927
+ :param session: Database session to use.
928
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
929
+ """
930
+
931
+ try:
932
+ for request_id in request_ids:
933
+ try:
934
+ set_request_state(request_id, new_state, session=session, logger=logger)
935
+ except UnsupportedOperation:
936
+ continue
937
+ except IntegrityError as error:
938
+ raise RucioException(error.args)
939
+
940
+
941
+ @METRICS.count_it
942
+ @transactional_session
943
+ def touch_requests_by_rule(rule_id, *, session: "Session"):
944
+ """
945
+ Update the update time of requests in a rule. Fails silently if no requests on this rule.
946
+
947
+ :param rule_id: Rule-ID as a 32 character hex string.
948
+ :param session: Database session to use.
949
+ """
950
+
951
+ try:
952
+ stmt = update(
953
+ models.Request
954
+ ).prefix_with(
955
+ "/*+ INDEX(REQUESTS REQUESTS_RULEID_IDX) */", dialect='oracle'
956
+ ).where(
957
+ models.Request.rule_id == rule_id,
958
+ models.Request.state.in_([RequestState.FAILED, RequestState.DONE, RequestState.LOST, RequestState.NO_SOURCES, RequestState.ONLY_TAPE_SOURCES]),
959
+ models.Request.updated_at < datetime.datetime.utcnow()
960
+ ).execution_options(
961
+ synchronize_session=False
962
+ ).values(
963
+ updated_at=datetime.datetime.utcnow() + datetime.timedelta(minutes=20)
964
+ )
965
+ session.execute(stmt)
966
+ except IntegrityError as error:
967
+ raise RucioException(error.args)
968
+
969
+
970
+ @read_session
971
+ def get_request(request_id, *, session: "Session"):
972
+ """
973
+ Retrieve a request by its ID.
974
+
975
+ :param request_id: Request-ID as a 32 character hex string.
976
+ :param session: Database session to use.
977
+ :returns: Request as a dictionary.
978
+ """
979
+
980
+ try:
981
+ stmt = select(
982
+ models.Request
983
+ ).where(
984
+ models.Request.id == request_id
985
+ )
986
+ tmp = session.execute(stmt).scalar()
987
+
988
+ if not tmp:
989
+ return
990
+ else:
991
+ tmp = tmp.to_dict()
992
+ tmp['attributes'] = json.loads(str(tmp['attributes'] or '{}'))
993
+ return tmp
994
+ except IntegrityError as error:
995
+ raise RucioException(error.args)
996
+
997
+
998
+ @METRICS.count_it
999
+ @read_session
1000
+ def get_request_by_did(scope, name, rse_id, request_type=None, *, session: "Session"):
1001
+ """
1002
+ Retrieve a request by its DID for a destination RSE.
1003
+
1004
+ :param scope: The scope of the data identifier.
1005
+ :param name: The name of the data identifier.
1006
+ :param rse_id: The destination RSE ID of the request.
1007
+ :param request_type: The type of request as rucio.db.sqla.constants.RequestType.
1008
+ :param session: Database session to use.
1009
+ :returns: Request as a dictionary.
1010
+ """
1011
+
1012
+ try:
1013
+ stmt = select(
1014
+ models.Request
1015
+ ).where(
1016
+ models.Request.scope == scope,
1017
+ models.Request.name == name,
1018
+ models.Request.dest_rse_id == rse_id
1019
+ )
1020
+ if request_type:
1021
+ stmt = stmt.where(
1022
+ models.Request.request_type == request_type
1023
+ )
1024
+
1025
+ tmp = session.execute(stmt).scalar()
1026
+ if not tmp:
1027
+ raise RequestNotFound(f'No request found for DID {scope}:{name} at RSE {rse_id}')
1028
+ else:
1029
+ tmp = tmp.to_dict()
1030
+
1031
+ tmp['source_rse'] = get_rse_name(rse_id=tmp['source_rse_id'], session=session) if tmp['source_rse_id'] is not None else None
1032
+ tmp['dest_rse'] = get_rse_name(rse_id=tmp['dest_rse_id'], session=session) if tmp['dest_rse_id'] is not None else None
1033
+ tmp['attributes'] = json.loads(str(tmp['attributes'] or '{}'))
1034
+
1035
+ return tmp
1036
+ except IntegrityError as error:
1037
+ raise RucioException(error.args)
1038
+
1039
+
1040
+ @METRICS.count_it
1041
+ @read_session
1042
+ def get_request_history_by_did(scope, name, rse_id, request_type=None, *, session: "Session"):
1043
+ """
1044
+ Retrieve a historical request by its DID for a destination RSE.
1045
+
1046
+ :param scope: The scope of the data identifier.
1047
+ :param name: The name of the data identifier.
1048
+ :param rse_id: The destination RSE ID of the request.
1049
+ :param request_type: The type of request as rucio.db.sqla.constants.RequestType.
1050
+ :param session: Database session to use.
1051
+ :returns: Request as a dictionary.
1052
+ """
1053
+
1054
+ try:
1055
+ stmt = select(
1056
+ models.RequestHistory
1057
+ ).where(
1058
+ models.RequestHistory.scope == scope,
1059
+ models.RequestHistory.name == name,
1060
+ models.RequestHistory.dest_rse_id == rse_id
1061
+ )
1062
+ if request_type:
1063
+ stmt = stmt.where(
1064
+ models.RequestHistory.request_type == request_type
1065
+ )
1066
+
1067
+ tmp = session.execute(stmt).scalar()
1068
+ if not tmp:
1069
+ raise RequestNotFound(f'No request found for DID {scope}:{name} at RSE {rse_id}')
1070
+ else:
1071
+ tmp = tmp.to_dict()
1072
+
1073
+ tmp['source_rse'] = get_rse_name(rse_id=tmp['source_rse_id'], session=session) if tmp['source_rse_id'] is not None else None
1074
+ tmp['dest_rse'] = get_rse_name(rse_id=tmp['dest_rse_id'], session=session) if tmp['dest_rse_id'] is not None else None
1075
+
1076
+ return tmp
1077
+ except IntegrityError as error:
1078
+ raise RucioException(error.args)
1079
+
1080
+
1081
+ def is_intermediate_hop(request):
1082
+ """
1083
+ Check if the request is an intermediate hop in a multi-hop transfer.
1084
+ """
1085
+ if (request['attributes'] or {}).get('is_intermediate_hop'):
1086
+ return True
1087
+ return False
1088
+
1089
+
1090
+ @transactional_session
1091
+ def handle_failed_intermediate_hop(request, *, session: "Session"):
1092
+ """
1093
+ Perform housekeeping behind a failed intermediate hop
1094
+ """
1095
+ # mark all hops following this one (in any multihop path) as Failed
1096
+ new_state = RequestState.FAILED
1097
+ reason = 'Unused hop in multi-hop'
1098
+
1099
+ paths = fetch_paths(request['id'], session=session)
1100
+ dependent_requests = []
1101
+ for path in paths.values():
1102
+ idx = path.index(request['id'])
1103
+ dependent_requests.extend(path[idx + 1:])
1104
+
1105
+ if dependent_requests:
1106
+ stmt = update(
1107
+ models.Request
1108
+ ).where(
1109
+ models.Request.id.in_(dependent_requests),
1110
+ models.Request.state.in_([RequestState.QUEUED, RequestState.SUBMITTED]),
1111
+ ).execution_options(
1112
+ synchronize_session=False
1113
+ ).values(
1114
+ state=new_state,
1115
+ err_msg=get_transfer_error(new_state, reason=reason),
1116
+ )
1117
+ session.execute(stmt)
1118
+
1119
+
1120
+ @METRICS.count_it
1121
+ @transactional_session
1122
+ def archive_request(request_id, *, session: "Session"):
1123
+ """
1124
+ Move a request to the history table.
1125
+
1126
+ :param request_id: Request-ID as a 32 character hex string.
1127
+ :param session: Database session to use.
1128
+ """
1129
+
1130
+ req = get_request(request_id=request_id, session=session)
1131
+
1132
+ if req:
1133
+ hist_request = models.RequestHistory(id=req['id'],
1134
+ created_at=req['created_at'],
1135
+ request_type=req['request_type'],
1136
+ scope=req['scope'],
1137
+ name=req['name'],
1138
+ dest_rse_id=req['dest_rse_id'],
1139
+ source_rse_id=req['source_rse_id'],
1140
+ attributes=json.dumps(req['attributes']) if isinstance(req['attributes'], dict) else req['attributes'],
1141
+ state=req['state'],
1142
+ account=req['account'],
1143
+ external_id=req['external_id'],
1144
+ retry_count=req['retry_count'],
1145
+ err_msg=req['err_msg'],
1146
+ previous_attempt_id=req['previous_attempt_id'],
1147
+ external_host=req['external_host'],
1148
+ rule_id=req['rule_id'],
1149
+ activity=req['activity'],
1150
+ bytes=req['bytes'],
1151
+ md5=req['md5'],
1152
+ adler32=req['adler32'],
1153
+ dest_url=req['dest_url'],
1154
+ requested_at=req['requested_at'],
1155
+ submitted_at=req['submitted_at'],
1156
+ staging_started_at=req['staging_started_at'],
1157
+ staging_finished_at=req['staging_finished_at'],
1158
+ started_at=req['started_at'],
1159
+ estimated_started_at=req['estimated_started_at'],
1160
+ estimated_at=req['estimated_at'],
1161
+ transferred_at=req['transferred_at'],
1162
+ estimated_transferred_at=req['estimated_transferred_at'],
1163
+ transfertool=req['transfertool'])
1164
+ hist_request.save(session=session)
1165
+ try:
1166
+ time_diff = req['updated_at'] - req['created_at']
1167
+ time_diff_s = time_diff.seconds + time_diff.days * 24 * 3600
1168
+ METRICS.timer('archive_request_per_activity.{activity}').labels(activity=req['activity'].replace(' ', '_')).observe(time_diff_s)
1169
+ session.execute(
1170
+ delete(
1171
+ models.Source
1172
+ ).where(
1173
+ models.Source.request_id == request_id
1174
+ )
1175
+ )
1176
+ session.execute(
1177
+ delete(
1178
+ models.TransferHop
1179
+ ).where(
1180
+ or_(models.TransferHop.request_id == request_id,
1181
+ models.TransferHop.next_hop_request_id == request_id,
1182
+ models.TransferHop.initial_request_id == request_id)
1183
+ )
1184
+ )
1185
+ session.execute(
1186
+ delete(
1187
+ models.Request
1188
+ ).where(
1189
+ models.Request.id == request_id
1190
+ )
1191
+ )
1192
+ except IntegrityError as error:
1193
+ raise RucioException(error.args)
1194
+
1195
+
1196
+ @METRICS.count_it
1197
+ @transactional_session
1198
+ def cancel_request_did(scope, name, dest_rse_id, request_type=RequestType.TRANSFER, *, session: "Session", logger=logging.log):
1199
+ """
1200
+ Cancel a request based on a DID and request type.
1201
+
1202
+ :param scope: Data identifier scope as a string.
1203
+ :param name: Data identifier name as a string.
1204
+ :param dest_rse_id: RSE id as a string.
1205
+ :param request_type: Type of the request.
1206
+ :param session: Database session to use.
1207
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
1208
+ """
1209
+
1210
+ reqs = None
1211
+ try:
1212
+ stmt = select(
1213
+ models.Request.id,
1214
+ models.Request.external_id,
1215
+ models.Request.external_host
1216
+ ).where(
1217
+ models.Request.scope == scope,
1218
+ models.Request.name == name,
1219
+ models.Request.dest_rse_id == dest_rse_id,
1220
+ models.Request.request_type == request_type
1221
+ )
1222
+ reqs = session.execute(stmt).all()
1223
+ if not reqs:
1224
+ logger(logging.WARNING, 'Tried to cancel non-existant request for DID %s:%s at RSE %s' % (scope, name, get_rse_name(rse_id=dest_rse_id, session=session)))
1225
+ except IntegrityError as error:
1226
+ raise RucioException(error.args)
1227
+
1228
+ transfers_to_cancel = {}
1229
+ for req in reqs:
1230
+ # is there a transfer already in transfertool? if so, schedule to cancel them
1231
+ if req[1] is not None:
1232
+ transfers_to_cancel.setdefault(req[2], set()).add(req[1])
1233
+ archive_request(request_id=req[0], session=session)
1234
+ return transfers_to_cancel
1235
+
1236
+
1237
+ @read_session
1238
+ def get_sources(request_id, rse_id=None, *, session: "Session"):
1239
+ """
1240
+ Retrieve sources by its ID.
1241
+
1242
+ :param request_id: Request-ID as a 32 character hex string.
1243
+ :param rse_id: RSE ID as a 32 character hex string.
1244
+ :param session: Database session to use.
1245
+ :returns: Sources as a dictionary.
1246
+ """
1247
+
1248
+ try:
1249
+ stmt = select(
1250
+ models.Source
1251
+ ).where(
1252
+ models.Source.request_id == request_id
1253
+ )
1254
+ if rse_id:
1255
+ stmt = stmt.where(
1256
+ models.Source.rse_id == rse_id
1257
+ )
1258
+ tmp = session.execute(stmt).scalars().all()
1259
+ if not tmp:
1260
+ return
1261
+ else:
1262
+ result = []
1263
+ for t in tmp:
1264
+ t2 = t.to_dict()
1265
+ result.append(t2)
1266
+
1267
+ return result
1268
+ except IntegrityError as error:
1269
+ raise RucioException(error.args)
1270
+
1271
+
1272
+ @read_session
1273
+ def get_heavy_load_rses(threshold, *, session: "Session"):
1274
+ """
1275
+ Retrieve heavy load rses.
1276
+
1277
+ :param threshold: Threshold as an int.
1278
+ :param session: Database session to use.
1279
+ :returns: .
1280
+ """
1281
+ try:
1282
+ stmt = select(
1283
+ models.Source.rse_id,
1284
+ func.count(models.Source.rse_id).label('load')
1285
+ ).where(
1286
+ models.Source.is_using == true()
1287
+ ).group_by(
1288
+ models.Source.rse_id
1289
+ )
1290
+ results = session.execute(stmt).all()
1291
+
1292
+ if not results:
1293
+ return
1294
+
1295
+ result = []
1296
+ for t in results:
1297
+ if t[1] >= threshold:
1298
+ t2 = {'rse_id': t[0], 'load': t[1]}
1299
+ result.append(t2)
1300
+
1301
+ return result
1302
+ except IntegrityError as error:
1303
+ raise RucioException(error.args)
1304
+
1305
+
1306
+ @read_session
1307
+ def get_request_stats(state, *, session: "Session"):
1308
+ """
1309
+ Retrieve statistics about requests by destination, activity and state.
1310
+
1311
+ :param state: Request state.
1312
+ :param session: Database session to use.
1313
+ :returns: List of (activity, dest_rse_id, state, counter).
1314
+ """
1315
+
1316
+ if type(state) is not list:
1317
+ state = [state]
1318
+
1319
+ try:
1320
+ stmt = select(
1321
+ models.Request.account,
1322
+ models.Request.state,
1323
+ models.Request.dest_rse_id,
1324
+ models.Request.source_rse_id,
1325
+ models.Request.activity,
1326
+ func.count(1).label('counter'),
1327
+ func.sum(models.Request.bytes).label('bytes')
1328
+ ).with_hint(
1329
+ models.Request, "INDEX(REQUESTS REQUESTS_TYP_STA_UPD_IDX)", 'oracle'
1330
+ ).where(
1331
+ models.Request.state.in_(state),
1332
+ models.Request.request_type.in_([RequestType.TRANSFER, RequestType.STAGEIN, RequestType.STAGEOUT])
1333
+ ).group_by(
1334
+ models.Request.account,
1335
+ models.Request.state,
1336
+ models.Request.dest_rse_id,
1337
+ models.Request.source_rse_id,
1338
+ models.Request.activity,
1339
+ )
1340
+
1341
+ return session.execute(stmt).all()
1342
+
1343
+ except IntegrityError as error:
1344
+ raise RucioException(error.args)
1345
+
1346
+
1347
+ @transactional_session
1348
+ def release_waiting_requests_per_deadline(
1349
+ dest_rse_id: Optional[str] = None,
1350
+ source_rse_id: Optional[str] = None,
1351
+ deadline: int = 1,
1352
+ *,
1353
+ session: "Session",
1354
+ ):
1355
+ """
1356
+ Release waiting requests that were waiting too long and exceeded the maximum waiting time to be released.
1357
+ If the DID of a request is attached to a dataset, the oldest requested_at date of all requests related to the dataset will be used for checking and all requests of this dataset will be released.
1358
+ :param dest_rse_id: The destination RSE id.
1359
+ :param source_rse_id: The source RSE id.
1360
+ :param deadline: Maximal waiting time in hours until a dataset gets released.
1361
+ :param session: The database session.
1362
+ """
1363
+ amount_released_requests = 0
1364
+ if deadline:
1365
+ grouped_requests_subquery, filtered_requests_subquery = create_base_query_grouped_fifo(dest_rse_id=dest_rse_id, source_rse_id=source_rse_id, session=session)
1366
+ old_requests_subquery = select(
1367
+ grouped_requests_subquery.c.name,
1368
+ grouped_requests_subquery.c.scope,
1369
+ grouped_requests_subquery.c.oldest_requested_at
1370
+ ).where(
1371
+ grouped_requests_subquery.c.oldest_requested_at < datetime.datetime.utcnow() - datetime.timedelta(hours=deadline)
1372
+ ).subquery()
1373
+
1374
+ old_requests_subquery = select(
1375
+ filtered_requests_subquery.c.id
1376
+ ).join(
1377
+ old_requests_subquery,
1378
+ and_(filtered_requests_subquery.c.dataset_name == old_requests_subquery.c.name,
1379
+ filtered_requests_subquery.c.dataset_scope == old_requests_subquery.c.scope)
1380
+ ).subquery()
1381
+
1382
+ amount_released_requests = update(
1383
+ models.Request
1384
+ ).where(
1385
+ models.Request.id.in_(old_requests_subquery)
1386
+ ).execution_options(
1387
+ synchronize_session=False
1388
+ ).values(
1389
+ {models.Request.state: RequestState.QUEUED}
1390
+ )
1391
+ return session.execute(amount_released_requests).rowcount
1392
+
1393
+
1394
+ @transactional_session
1395
+ def release_waiting_requests_per_free_volume(
1396
+ dest_rse_id: Optional[str] = None,
1397
+ source_rse_id: Optional[str] = None,
1398
+ volume: int = 0,
1399
+ *,
1400
+ session: "Session"
1401
+ ):
1402
+ """
1403
+ Release waiting requests if they fit in available transfer volume. If the DID of a request is attached to a dataset, the volume will be checked for the whole dataset as all requests related to this dataset will be released.
1404
+
1405
+ :param dest_rse_id: The destination RSE id.
1406
+ :param source_rse_id: The source RSE id
1407
+ :param volume: The maximum volume in bytes that should be transfered.
1408
+ :param session: The database session.
1409
+ """
1410
+
1411
+ dialect = session.bind.dialect.name
1412
+ if dialect == 'mysql' or dialect == 'sqlite':
1413
+ coalesce_func = func.ifnull
1414
+ elif dialect == 'oracle':
1415
+ coalesce_func = func.nvl
1416
+ else: # dialect == 'postgresql'
1417
+ coalesce_func = func.coalesce
1418
+
1419
+ sum_volume_active_subquery = select(
1420
+ coalesce_func(func.sum(models.Request.bytes), 0).label('sum_bytes')
1421
+ ).where(
1422
+ models.Request.state.in_([RequestState.SUBMITTED, RequestState.QUEUED]),
1423
+ )
1424
+ if dest_rse_id is not None:
1425
+ sum_volume_active_subquery = sum_volume_active_subquery.where(
1426
+ models.Request.dest_rse_id == dest_rse_id
1427
+ )
1428
+ if source_rse_id is not None:
1429
+ sum_volume_active_subquery = sum_volume_active_subquery.where(
1430
+ models.Request.source_rse_id == source_rse_id
1431
+ )
1432
+ sum_volume_active_subquery = sum_volume_active_subquery.subquery()
1433
+
1434
+ grouped_requests_subquery, filtered_requests_subquery = create_base_query_grouped_fifo(dest_rse_id=dest_rse_id, source_rse_id=source_rse_id, session=session)
1435
+
1436
+ cumulated_volume_subquery = select(
1437
+ grouped_requests_subquery.c.name,
1438
+ grouped_requests_subquery.c.scope,
1439
+ func.sum(grouped_requests_subquery.c.volume).over(order_by=grouped_requests_subquery.c.oldest_requested_at).label('cum_volume')
1440
+ ).where(
1441
+ grouped_requests_subquery.c.volume <= volume - sum_volume_active_subquery.c.sum_bytes
1442
+ ).subquery()
1443
+
1444
+ cumulated_volume_subquery = select(
1445
+ filtered_requests_subquery.c.id
1446
+ ).join(
1447
+ cumulated_volume_subquery,
1448
+ and_(filtered_requests_subquery.c.dataset_name == cumulated_volume_subquery.c.name,
1449
+ filtered_requests_subquery.c.dataset_scope == cumulated_volume_subquery.c.scope)
1450
+ ).where(
1451
+ cumulated_volume_subquery.c.cum_volume <= volume - sum_volume_active_subquery.c.sum_bytes
1452
+ ).subquery()
1453
+
1454
+ amount_released_requests = update(
1455
+ models.Request
1456
+ ).where(
1457
+ models.Request.id.in_(cumulated_volume_subquery)
1458
+ ).execution_options(
1459
+ synchronize_session=False
1460
+ ).values(
1461
+ {models.Request.state: RequestState.QUEUED},
1462
+ )
1463
+ return session.execute(amount_released_requests).rowcount
1464
+
1465
+
1466
+ @read_session
1467
+ def create_base_query_grouped_fifo(
1468
+ dest_rse_id: Optional[str] = None,
1469
+ source_rse_id: Optional[str] = None,
1470
+ *,
1471
+ session: "Session"
1472
+ ):
1473
+ """
1474
+ Build the sqlalchemy queries to filter relevant requests and to group them in datasets.
1475
+ Group requests either by same destination RSE or source RSE.
1476
+
1477
+ :param dest_rse_id: The source RSE id to filter on
1478
+ :param source_rse_id: The destination RSE id to filter on
1479
+ :param session: The database session.
1480
+ """
1481
+ dialect = session.bind.dialect.name
1482
+ if dialect == 'mysql' or dialect == 'sqlite':
1483
+ coalesce_func = func.ifnull
1484
+ elif dialect == 'oracle':
1485
+ coalesce_func = func.nvl
1486
+ else: # dialect == 'postgresql'
1487
+ coalesce_func = func.coalesce
1488
+
1489
+ # query DIDs that are attached to a collection and add a column indicating the order of attachment in case of mulitple attachments
1490
+ attachment_order_subquery = select(
1491
+ models.DataIdentifierAssociation.child_name,
1492
+ models.DataIdentifierAssociation.child_scope,
1493
+ models.DataIdentifierAssociation.name,
1494
+ models.DataIdentifierAssociation.scope,
1495
+ func.row_number().over(
1496
+ partition_by=(models.DataIdentifierAssociation.child_name,
1497
+ models.DataIdentifierAssociation.child_scope),
1498
+ order_by=models.DataIdentifierAssociation.created_at
1499
+ ).label('order_of_attachment')
1500
+ ).subquery()
1501
+
1502
+ # query transfer requests and join with according datasets
1503
+ requests_subquery_stmt = select(
1504
+ # Will be filled using add_columns() later
1505
+ ).outerjoin(
1506
+ attachment_order_subquery,
1507
+ and_(models.Request.name == attachment_order_subquery.c.child_name,
1508
+ models.Request.scope == attachment_order_subquery.c.child_scope,
1509
+ attachment_order_subquery.c.order_of_attachment == 1),
1510
+ ).where(
1511
+ models.Request.state == RequestState.WAITING,
1512
+ )
1513
+ if source_rse_id is not None:
1514
+ requests_subquery_stmt = requests_subquery_stmt.where(
1515
+ models.Request.source_rse_id == source_rse_id
1516
+ )
1517
+ if dest_rse_id is not None:
1518
+ requests_subquery_stmt = requests_subquery_stmt.where(
1519
+ models.Request.dest_rse_id == dest_rse_id
1520
+ )
1521
+
1522
+ filtered_requests_subquery = requests_subquery_stmt.add_columns(
1523
+ coalesce_func(attachment_order_subquery.c.scope, models.Request.scope).label('dataset_scope'),
1524
+ coalesce_func(attachment_order_subquery.c.name, models.Request.name).label('dataset_name'),
1525
+ models.Request.id.label('id')
1526
+ ).subquery()
1527
+
1528
+ combined_attached_unattached_requests = requests_subquery_stmt.add_columns(
1529
+ coalesce_func(attachment_order_subquery.c.scope, models.Request.scope).label('scope'),
1530
+ coalesce_func(attachment_order_subquery.c.name, models.Request.name).label('name'),
1531
+ models.Request.bytes,
1532
+ models.Request.requested_at
1533
+ ).subquery()
1534
+
1535
+ # group requests and calculate properties like oldest requested_at, amount of children, volume
1536
+ grouped_requests_subquery = select(
1537
+ func.sum(combined_attached_unattached_requests.c.bytes).label('volume'),
1538
+ func.min(combined_attached_unattached_requests.c.requested_at).label('oldest_requested_at'),
1539
+ func.count().label('amount_childs'),
1540
+ combined_attached_unattached_requests.c.name,
1541
+ combined_attached_unattached_requests.c.scope
1542
+ ).group_by(
1543
+ combined_attached_unattached_requests.c.scope,
1544
+ combined_attached_unattached_requests.c.name
1545
+ ).subquery()
1546
+ return grouped_requests_subquery, filtered_requests_subquery
1547
+
1548
+
1549
+ @transactional_session
1550
+ def release_waiting_requests_fifo(
1551
+ dest_rse_id: Optional[str] = None,
1552
+ source_rse_id: Optional[str] = None,
1553
+ activity: Optional[str] = None,
1554
+ count: int = 0,
1555
+ account: Optional[InternalAccount] = None,
1556
+ *,
1557
+ session: "Session"
1558
+ ):
1559
+ """
1560
+ Release waiting requests. Transfer requests that were requested first, get released first (FIFO).
1561
+
1562
+ :param source_rse_id: The source rse id
1563
+ :param dest_rse_id: The destination rse id
1564
+ :param activity: The activity.
1565
+ :param count: The count to be released.
1566
+ :param account: The account name whose requests to release.
1567
+ :param session: The database session.
1568
+ """
1569
+
1570
+ dialect = session.bind.dialect.name
1571
+ rowcount = 0
1572
+
1573
+ subquery = select(
1574
+ models.Request.id
1575
+ ).where(
1576
+ models.Request.state == RequestState.WAITING
1577
+ ).order_by(
1578
+ asc(models.Request.requested_at)
1579
+ ).limit(
1580
+ count
1581
+ )
1582
+ if source_rse_id is not None:
1583
+ subquery = subquery.where(models.Request.source_rse_id == source_rse_id)
1584
+ if dest_rse_id is not None:
1585
+ subquery = subquery.where(models.Request.dest_rse_id == dest_rse_id)
1586
+
1587
+ if activity is not None:
1588
+ subquery = subquery.where(models.Request.activity == activity)
1589
+ if account is not None:
1590
+ subquery = subquery.where(models.Request.account == account)
1591
+
1592
+ subquery = subquery.subquery()
1593
+
1594
+ if dialect == 'mysql':
1595
+ # TODO: check if the logic from this `if` is still needed on modern mysql
1596
+
1597
+ # join because IN and LIMIT cannot be used together
1598
+ subquery = select(
1599
+ models.Request.id
1600
+ ).join(
1601
+ subquery,
1602
+ models.Request.id == subquery.c.id
1603
+ ).subquery()
1604
+ # wrap select to update and select from the same table
1605
+ subquery = select(subquery.c.id).subquery()
1606
+
1607
+ stmt = update(
1608
+ models.Request
1609
+ ).where(
1610
+ models.Request.id.in_(subquery)
1611
+ ).execution_options(
1612
+ synchronize_session=False
1613
+ ).values(
1614
+ {'state': RequestState.QUEUED}
1615
+ )
1616
+ rowcount = session.execute(stmt).rowcount
1617
+ return rowcount
1618
+
1619
+
1620
+ @transactional_session
1621
+ def release_waiting_requests_grouped_fifo(
1622
+ dest_rse_id: Optional[str] = None,
1623
+ source_rse_id: Optional[str] = None,
1624
+ count: int = 0,
1625
+ deadline: int = 1,
1626
+ volume: int = 0,
1627
+ *,
1628
+ session: "Session"
1629
+ ):
1630
+ """
1631
+ Release waiting requests. Transfer requests that were requested first, get released first (FIFO).
1632
+ Also all requests to DIDs that are attached to the same dataset get released, if one children of the dataset is choosed to be released (Grouped FIFO).
1633
+
1634
+ :param dest_rse_id: The destination rse id
1635
+ :param source_rse_id: The source RSE id.
1636
+ :param count: The count to be released. If None, release all waiting requests.
1637
+ :param deadline: Maximal waiting time in hours until a dataset gets released.
1638
+ :param volume: The maximum volume in bytes that should be transfered.
1639
+ :param session: The database session.
1640
+ """
1641
+
1642
+ amount_updated_requests = 0
1643
+
1644
+ # Release requests that exceeded waiting time
1645
+ if deadline and source_rse_id is not None:
1646
+ amount_updated_requests = release_waiting_requests_per_deadline(dest_rse_id=dest_rse_id, source_rse_id=source_rse_id, deadline=deadline, session=session)
1647
+ count = count - amount_updated_requests
1648
+
1649
+ grouped_requests_subquery, filtered_requests_subquery = create_base_query_grouped_fifo(dest_rse_id=dest_rse_id, source_rse_id=source_rse_id, session=session)
1650
+
1651
+ # cumulate amount of children per dataset and combine with each request and only keep requests that dont exceed the limit
1652
+ cumulated_children_subquery = select(
1653
+ grouped_requests_subquery.c.name,
1654
+ grouped_requests_subquery.c.scope,
1655
+ grouped_requests_subquery.c.amount_childs,
1656
+ grouped_requests_subquery.c.oldest_requested_at,
1657
+ func.sum(grouped_requests_subquery.c.amount_childs).over(order_by=(grouped_requests_subquery.c.oldest_requested_at)).label('cum_amount_childs')
1658
+ ).subquery()
1659
+ cumulated_children_subquery = select(
1660
+ filtered_requests_subquery.c.id
1661
+ ).join(
1662
+ cumulated_children_subquery,
1663
+ and_(filtered_requests_subquery.c.dataset_name == cumulated_children_subquery.c.name,
1664
+ filtered_requests_subquery.c.dataset_scope == cumulated_children_subquery.c.scope)
1665
+ ).where(
1666
+ cumulated_children_subquery.c.cum_amount_childs - cumulated_children_subquery.c.amount_childs < count
1667
+ ).subquery()
1668
+
1669
+ # needed for mysql to update and select from the same table
1670
+ cumulated_children_subquery = select(cumulated_children_subquery.c.id).subquery()
1671
+
1672
+ stmt = update(
1673
+ models.Request
1674
+ ).where(
1675
+ models.Request.id.in_(cumulated_children_subquery)
1676
+ ).execution_options(
1677
+ synchronize_session=False
1678
+ ).values(
1679
+ {models.Request.state: RequestState.QUEUED}
1680
+ )
1681
+ amount_updated_requests += session.execute(stmt).rowcount
1682
+
1683
+ # release requests where the whole datasets volume fits in the available volume space
1684
+ if volume and dest_rse_id is not None:
1685
+ amount_updated_requests += release_waiting_requests_per_free_volume(dest_rse_id=dest_rse_id, volume=volume, session=session)
1686
+
1687
+ return amount_updated_requests
1688
+
1689
+
1690
+ @transactional_session
1691
+ def release_all_waiting_requests(
1692
+ dest_rse_id: Optional[str] = None,
1693
+ source_rse_id: Optional[str] = None,
1694
+ activity: Optional[str] = None,
1695
+ account: Optional[InternalAccount] = None,
1696
+ *,
1697
+ session: "Session"
1698
+ ):
1699
+ """
1700
+ Release all waiting requests per destination RSE.
1701
+
1702
+ :param dest_rse_id: The destination rse id.
1703
+ :param source_rse_id: The source rse id.
1704
+ :param activity: The activity.
1705
+ :param account: The account name whose requests to release.
1706
+ :param session: The database session.
1707
+ """
1708
+ try:
1709
+ query = update(
1710
+ models.Request
1711
+ ).where(
1712
+ models.Request.state == RequestState.WAITING,
1713
+ ).execution_options(
1714
+ synchronize_session=False
1715
+ ).values(
1716
+ {'state': RequestState.QUEUED}
1717
+ )
1718
+ if source_rse_id is not None:
1719
+ query = query.where(
1720
+ models.Request.source_rse_id == source_rse_id
1721
+ )
1722
+ if dest_rse_id is not None:
1723
+ query = query.where(
1724
+ models.Request.dest_rse_id == dest_rse_id
1725
+ )
1726
+ if activity is not None:
1727
+ query = query.where(
1728
+ models.Request.activity == activity
1729
+ )
1730
+ if account is not None:
1731
+ query = query.where(
1732
+ models.Request.account == account
1733
+ )
1734
+ rowcount = session.execute(query).rowcount
1735
+ return rowcount
1736
+ except IntegrityError as error:
1737
+ raise RucioException(error.args)
1738
+
1739
+
1740
+ @stream_session
1741
+ def list_transfer_limits(
1742
+ *,
1743
+ session: "Session",
1744
+ ):
1745
+ stmt = select(
1746
+ models.TransferLimit
1747
+ )
1748
+ for limit in session.execute(stmt).scalars():
1749
+ dict_resp = limit.to_dict()
1750
+ yield dict_resp
1751
+
1752
+
1753
+ def _sync_rse_transfer_limit(
1754
+ limit_id: Union[str, uuid.UUID],
1755
+ desired_rse_ids: set[str],
1756
+ *,
1757
+ session: "Session",
1758
+ ):
1759
+ """
1760
+ Ensure that an RSETransferLimit exists in the database for each of the given rses (and only for these rses)
1761
+ """
1762
+
1763
+ stmt = select(
1764
+ models.RSETransferLimit.rse_id,
1765
+ ).where(
1766
+ models.RSETransferLimit.limit_id == limit_id
1767
+ )
1768
+ existing_rse_ids = set(session.execute(stmt).scalars())
1769
+
1770
+ rse_limits_to_add = desired_rse_ids.difference(existing_rse_ids)
1771
+ rse_limits_to_delete = existing_rse_ids.difference(desired_rse_ids)
1772
+
1773
+ if rse_limits_to_add:
1774
+ session.execute(
1775
+ insert(models.RSETransferLimit),
1776
+ [
1777
+ {'rse_id': rse_id, 'limit_id': limit_id}
1778
+ for rse_id in rse_limits_to_add
1779
+ ]
1780
+ )
1781
+
1782
+ if rse_limits_to_delete:
1783
+ stmt = delete(
1784
+ models.RSETransferLimit
1785
+ ).where(
1786
+ models.RSETransferLimit.limit_id == limit_id,
1787
+ models.RSETransferLimit.rse_id.in_(rse_limits_to_delete)
1788
+ )
1789
+ session.execute(stmt)
1790
+
1791
+
1792
+ @transactional_session
1793
+ def re_sync_all_transfer_limits(
1794
+ delete_empty: bool = False,
1795
+ *,
1796
+ session: "Session",
1797
+ ):
1798
+ """
1799
+ For each TransferLimit in the database, re-evaluate the rse expression and ensure that the
1800
+ correct RSETransferLimits are in the database
1801
+ :param delete_empty: if True, when rse_expression evaluates to an empty set or is invalid, the limit is completely removed
1802
+ """
1803
+ stmt = select(
1804
+ models.TransferLimit,
1805
+ )
1806
+ for limit in session.execute(stmt).scalars():
1807
+ try:
1808
+ desired_rse_ids = {rse['id'] for rse in parse_expression(expression=limit.rse_expression, session=session)}
1809
+ except InvalidRSEExpression:
1810
+ desired_rse_ids = set()
1811
+
1812
+ if not desired_rse_ids and delete_empty:
1813
+ delete_transfer_limit_by_id(limit_id=limit.id, session=session)
1814
+ else:
1815
+ _sync_rse_transfer_limit(limit_id=limit.id, desired_rse_ids=desired_rse_ids, session=session)
1816
+
1817
+
1818
+ @transactional_session
1819
+ def set_transfer_limit(
1820
+ rse_expression: str,
1821
+ activity: Optional[str] = None,
1822
+ direction: TransferLimitDirection = TransferLimitDirection.DESTINATION,
1823
+ max_transfers: Optional[int] = None,
1824
+ volume: Optional[int] = None,
1825
+ deadline: Optional[int] = None,
1826
+ strategy: Optional[str] = None,
1827
+ transfers: Optional[int] = None,
1828
+ waitings: Optional[int] = None,
1829
+ *,
1830
+ session: "Session",
1831
+ ):
1832
+ """
1833
+ Create or update a transfer limit
1834
+
1835
+ :param rse_expression: RSE expression string.
1836
+ :param activity: The activity.
1837
+ :param direction: The direction in which this limit applies (source/destination)
1838
+ :param max_transfers: Maximum transfers.
1839
+ :param volume: Maximum transfer volume in bytes.
1840
+ :param deadline: Maximum waiting time in hours until a datasets gets released.
1841
+ :param strategy: defines how to handle datasets: `fifo` (each file released separately) or `grouped_fifo` (wait for the entire dataset to fit)
1842
+ :param transfers: Current number of active transfers
1843
+ :param waitings: Current number of waiting transfers
1844
+ :param session: The database session in use.
1845
+
1846
+ :return: the limit id
1847
+ """
1848
+ if activity is None:
1849
+ activity = 'all_activities'
1850
+
1851
+ stmt = select(
1852
+ models.TransferLimit
1853
+ ).where(
1854
+ models.TransferLimit.rse_expression == rse_expression,
1855
+ models.TransferLimit.activity == activity,
1856
+ models.TransferLimit.direction == direction
1857
+ )
1858
+ limit = session.execute(stmt).scalar_one_or_none()
1859
+
1860
+ if not limit:
1861
+ if max_transfers is None:
1862
+ max_transfers = 0
1863
+ if volume is None:
1864
+ volume = 0
1865
+ if deadline is None:
1866
+ deadline = 1
1867
+ if strategy is None:
1868
+ strategy = 'fifo'
1869
+ limit = models.TransferLimit(
1870
+ rse_expression=rse_expression,
1871
+ activity=activity,
1872
+ direction=direction,
1873
+ max_transfers=max_transfers,
1874
+ volume=volume,
1875
+ deadline=deadline,
1876
+ strategy=strategy,
1877
+ transfers=transfers,
1878
+ waitings=waitings
1879
+ )
1880
+ limit.save(session=session)
1881
+ else:
1882
+ changed = False
1883
+ if max_transfers is not None and limit.max_transfers != max_transfers:
1884
+ limit.max_transfers = max_transfers
1885
+ changed = True
1886
+ if volume is not None and limit.volume != volume:
1887
+ limit.volume = volume
1888
+ changed = True
1889
+ if deadline is not None and limit.deadline != deadline:
1890
+ limit.deadline = deadline
1891
+ changed = True
1892
+ if strategy is not None and limit.strategy != strategy:
1893
+ limit.strategy = strategy
1894
+ changed = True
1895
+ if transfers is not None and limit.transfers != transfers:
1896
+ limit.transfers = transfers
1897
+ changed = True
1898
+ if waitings is not None and limit.waitings != waitings:
1899
+ limit.waitings = waitings
1900
+ changed = True
1901
+ if changed:
1902
+ limit.save(session=session)
1903
+
1904
+ desired_rse_ids = {rse['id'] for rse in parse_expression(expression=rse_expression, session=session)}
1905
+ _sync_rse_transfer_limit(limit_id=limit.id, desired_rse_ids=desired_rse_ids, session=session)
1906
+ return limit.id
1907
+
1908
+
1909
+ @transactional_session
1910
+ def set_transfer_limit_stats(
1911
+ limit_id: str,
1912
+ waitings: int,
1913
+ transfers: int,
1914
+ *,
1915
+ session: "Session",
1916
+ ):
1917
+ """
1918
+ Set the statistics of the TransferLimit
1919
+ """
1920
+ stmt = update(
1921
+ models.TransferLimit
1922
+ ).where(
1923
+ models.TransferLimit.id == limit_id
1924
+ ).values(
1925
+ waitings=waitings,
1926
+ transfers=transfers
1927
+ )
1928
+ session.execute(stmt)
1929
+
1930
+
1931
+ @transactional_session
1932
+ def delete_transfer_limit(
1933
+ rse_expression: str,
1934
+ activity: Optional[str] = None,
1935
+ direction: TransferLimitDirection = TransferLimitDirection.DESTINATION,
1936
+ *,
1937
+ session: "Session",
1938
+ ):
1939
+
1940
+ if activity is None:
1941
+ activity = 'all_activities'
1942
+
1943
+ stmt = delete(
1944
+ models.RSETransferLimit
1945
+ ).where(
1946
+ exists(
1947
+ select(1)
1948
+ ).where(
1949
+ models.RSETransferLimit.limit_id == models.TransferLimit.id,
1950
+ models.TransferLimit.rse_expression == rse_expression,
1951
+ models.TransferLimit.activity == activity,
1952
+ models.TransferLimit.direction == direction
1953
+ )
1954
+ ).execution_options(
1955
+ synchronize_session=False
1956
+ )
1957
+ session.execute(stmt)
1958
+
1959
+ stmt = delete(
1960
+ models.TransferLimit
1961
+ ).where(
1962
+ models.TransferLimit.rse_expression == rse_expression,
1963
+ models.TransferLimit.activity == activity,
1964
+ models.TransferLimit.direction == direction
1965
+ )
1966
+ session.execute(stmt)
1967
+
1968
+
1969
+ @transactional_session
1970
+ def delete_transfer_limit_by_id(
1971
+ limit_id: str,
1972
+ *,
1973
+ session: "Session",
1974
+ ):
1975
+ stmt = delete(
1976
+ models.RSETransferLimit
1977
+ ).where(
1978
+ models.RSETransferLimit.limit_id == limit_id
1979
+ )
1980
+ session.execute(stmt)
1981
+
1982
+ stmt = delete(
1983
+ models.TransferLimit
1984
+ ).where(
1985
+ models.TransferLimit.id == limit_id
1986
+ )
1987
+ session.execute(stmt)
1988
+
1989
+
1990
+ @transactional_session
1991
+ def update_requests_priority(priority, filter_, *, session: "Session", logger=logging.log):
1992
+ """
1993
+ Update priority of requests.
1994
+
1995
+ :param priority: The priority as an integer from 1 to 5.
1996
+ :param filter_: Dictionary such as {'rule_id': rule_id, 'request_id': request_id, 'older_than': time_stamp, 'activities': [activities]}.
1997
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
1998
+ :return the transfers which must be updated in the transfertool
1999
+ """
2000
+ try:
2001
+ query = select(
2002
+ models.Request.id,
2003
+ models.Request.external_id,
2004
+ models.Request.external_host,
2005
+ models.Request.state.label('request_state'),
2006
+ models.ReplicaLock.state.label('lock_state')
2007
+ ).join(
2008
+ models.ReplicaLock,
2009
+ and_(models.ReplicaLock.scope == models.Request.scope,
2010
+ models.ReplicaLock.name == models.Request.name,
2011
+ models.ReplicaLock.rse_id == models.Request.dest_rse_id)
2012
+ )
2013
+ if 'rule_id' in filter_:
2014
+ query = query.filter(models.ReplicaLock.rule_id == filter_['rule_id'])
2015
+ if 'request_id' in filter_:
2016
+ query = query.filter(models.Request.id == filter_['request_id'])
2017
+ if 'older_than' in filter_:
2018
+ query = query.filter(models.Request.created_at < filter_['older_than'])
2019
+ if 'activities' in filter_:
2020
+ if type(filter_['activities']) is not list:
2021
+ filter_['activities'] = filter_['activities'].split(',')
2022
+ query = query.filter(models.Request.activity.in_(filter_['activities']))
2023
+
2024
+ transfers_to_update = {}
2025
+ for item in session.execute(query).all():
2026
+ try:
2027
+ update_request(item.id, priority=priority, session=session)
2028
+ logger(logging.DEBUG, "Updated request %s priority to %s in rucio." % (item.id, priority))
2029
+ if item.request_state == RequestState.SUBMITTED and item.lock_state == LockState.REPLICATING:
2030
+ transfers_to_update.setdefault(item.external_host, {})[item.external_id] = priority
2031
+ except Exception:
2032
+ logger(logging.DEBUG, "Failed to boost request %s priority: %s" % (item.id, traceback.format_exc()))
2033
+ return transfers_to_update
2034
+ except IntegrityError as error:
2035
+ raise RucioException(error.args)
2036
+
2037
+
2038
+ @read_session
2039
+ def add_monitor_message(new_state, request, additional_fields, *, session: "Session"):
2040
+ """
2041
+ Create a message for hermes from a request
2042
+
2043
+ :param new_state: The new state of the transfer request
2044
+ :param request: The request to create the message for.
2045
+ :param additional_fields: Additional custom fields to be added to the message
2046
+ :param session: The database session to use.
2047
+ """
2048
+
2049
+ if request['request_type']:
2050
+ transfer_status = '%s-%s' % (request['request_type'].name, new_state.name)
2051
+ else:
2052
+ transfer_status = 'transfer-%s' % new_state.name
2053
+ transfer_status = transfer_status.lower()
2054
+
2055
+ stmt = select(
2056
+ models.DataIdentifier.datatype
2057
+ ).where(
2058
+ models.DataIdentifier.scope == request['scope'],
2059
+ models.DataIdentifier.name == request['name'],
2060
+ )
2061
+ datatype = session.execute(stmt).scalar_one_or_none()
2062
+
2063
+ # Start by filling up fields from database request or with defaults.
2064
+ message = {'activity': request.get('activity', None),
2065
+ 'request-id': request['id'],
2066
+ 'duration': -1,
2067
+ 'checksum-adler': request.get('adler32', None),
2068
+ 'checksum-md5': request.get('md5', None),
2069
+ 'file-size': request.get('bytes', None),
2070
+ 'bytes': request.get('bytes', None),
2071
+ 'guid': None,
2072
+ 'previous-request-id': request['previous_attempt_id'],
2073
+ 'protocol': None,
2074
+ 'scope': request['scope'],
2075
+ 'name': request['name'],
2076
+ 'dataset': None,
2077
+ 'datasetScope': None,
2078
+ 'src-type': None,
2079
+ 'src-rse': request.get('source_rse', None),
2080
+ 'src-url': None,
2081
+ 'dst-type': None,
2082
+ 'dst-rse': request.get('dest_rse', None),
2083
+ 'dst-url': request.get('dest_url', None),
2084
+ 'reason': request.get('err_msg', None),
2085
+ 'transfer-endpoint': request['external_host'],
2086
+ 'transfer-id': request['external_id'],
2087
+ 'transfer-link': None,
2088
+ 'created_at': request.get('created_at', None),
2089
+ 'submitted_at': request.get('submitted_at', None),
2090
+ 'started_at': request.get('started_at', None),
2091
+ 'transferred_at': request.get('transferred_at', None),
2092
+ 'tool-id': 'rucio-conveyor',
2093
+ 'account': request.get('account', None),
2094
+ 'datatype': datatype}
2095
+
2096
+ # Add (or override) existing fields
2097
+ message.update(additional_fields)
2098
+
2099
+ if message['started_at'] and message['transferred_at']:
2100
+ message['duration'] = (message['transferred_at'] - message['started_at']).seconds
2101
+ ds_scope = request['attributes'].get('ds_scope')
2102
+ if not message['datasetScope'] and ds_scope:
2103
+ message['datasetScope'] = ds_scope
2104
+ ds_name = request['attributes'].get('ds_name')
2105
+ if not message['dataset'] and ds_name:
2106
+ message['dataset'] = ds_name
2107
+ if not message.get('protocol'):
2108
+ dst_url = message['dst-url']
2109
+ if dst_url and ':' in dst_url:
2110
+ message['protocol'] = dst_url.split(':')[0]
2111
+ elif request.get('transfertool'):
2112
+ message['protocol'] = request['transfertool']
2113
+ if not message.get('src-rse'):
2114
+ src_rse_id = request.get('source_rse_id', None)
2115
+ if src_rse_id:
2116
+ src_rse = get_rse_name(src_rse_id, session=session)
2117
+ message['src-rse'] = src_rse
2118
+ if not message.get('dst-rse'):
2119
+ dst_rse_id = request.get('dest_rse_id', None)
2120
+ if dst_rse_id:
2121
+ dst_rse = get_rse_name(dst_rse_id, session=session)
2122
+ message['dst-rse'] = dst_rse
2123
+ if not message.get('vo') and request.get('source_rse_id'):
2124
+ src_id = request['source_rse_id']
2125
+ vo = get_rse_vo(rse_id=src_id, session=session)
2126
+ if vo != 'def':
2127
+ message['vo'] = vo
2128
+ for time_field in ('created_at', 'submitted_at', 'started_at', 'transferred_at'):
2129
+ field_value = message[time_field]
2130
+ message[time_field] = str(field_value) if field_value else None
2131
+
2132
+ add_message(transfer_status, message, session=session)
2133
+
2134
+
2135
+ def get_transfer_error(state, reason=None):
2136
+ """
2137
+ Transform a specific RequestState to an error message
2138
+
2139
+ :param state: State of the request.
2140
+ :param reason: Reason of the state.
2141
+ :returns: Error message
2142
+ """
2143
+ err_msg = None
2144
+ if state in [RequestState.NO_SOURCES, RequestState.ONLY_TAPE_SOURCES]:
2145
+ err_msg = '%s:%s' % (RequestErrMsg.NO_SOURCES, state)
2146
+ elif state in [RequestState.SUBMISSION_FAILED]:
2147
+ err_msg = '%s:%s' % (RequestErrMsg.SUBMISSION_FAILED, state)
2148
+ elif state in [RequestState.SUBMITTING]:
2149
+ err_msg = '%s:%s' % (RequestErrMsg.SUBMISSION_FAILED, "Too long time in submitting state")
2150
+ elif state in [RequestState.LOST]:
2151
+ err_msg = '%s:%s' % (RequestErrMsg.TRANSFER_FAILED, "Transfer job on FTS is lost")
2152
+ elif state in [RequestState.FAILED]:
2153
+ err_msg = '%s:%s' % (RequestErrMsg.TRANSFER_FAILED, reason)
2154
+ elif state in [RequestState.MISMATCH_SCHEME]:
2155
+ err_msg = '%s:%s' % (RequestErrMsg.MISMATCH_SCHEME, state)
2156
+ return err_msg
2157
+
2158
+
2159
+ @read_session
2160
+ def get_source_rse(request_id, src_url, *, session: "Session", logger=logging.log):
2161
+ """
2162
+ Based on a request, and src_url extract the source rse name and id.
2163
+
2164
+ :param request_id: The request_id of the request.
2165
+ :param src_url: The src_url of the request.
2166
+ :param session: The database session to use.
2167
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
2168
+ """
2169
+
2170
+ try:
2171
+ if not request_id:
2172
+ return None, None
2173
+
2174
+ sources = get_sources(request_id, session=session)
2175
+ sources = sources or []
2176
+ for source in sources:
2177
+ if source['url'] == src_url:
2178
+ src_rse_id = source['rse_id']
2179
+ src_rse_name = get_rse_name(src_rse_id, session=session)
2180
+ logger(logging.DEBUG, "Find rse name %s for %s" % (src_rse_name, src_url))
2181
+ return src_rse_name, src_rse_id
2182
+ # cannot find matched surl
2183
+ logger(logging.WARNING, 'Cannot get correct RSE for source url: %s' % (src_url))
2184
+ return None, None
2185
+ except Exception:
2186
+ logger(logging.ERROR, 'Cannot get correct RSE for source url: %s' % (src_url), exc_info=True)
2187
+ return None, None
2188
+
2189
+
2190
+ @stream_session
2191
+ def list_requests(src_rse_ids, dst_rse_ids, states=None, *, session: "Session"):
2192
+ """
2193
+ List all requests in a specific state from a source RSE to a destination RSE.
2194
+
2195
+ :param src_rse_ids: source RSE ids.
2196
+ :param dst_rse_ids: destination RSE ids.
2197
+ :param states: list of request states.
2198
+ :param session: The database session in use.
2199
+ """
2200
+ if not states:
2201
+ states = [RequestState.WAITING]
2202
+
2203
+ stmt = select(
2204
+ models.Request
2205
+ ).where(
2206
+ models.Request.state.in_(states),
2207
+ models.Request.source_rse_id.in_(src_rse_ids),
2208
+ models.Request.dest_rse_id.in_(dst_rse_ids)
2209
+ )
2210
+ for request in session.execute(stmt).yield_per(500).scalars():
2211
+ yield request
2212
+
2213
+
2214
+ @stream_session
2215
+ def list_requests_history(src_rse_ids, dst_rse_ids, states=None, offset=None, limit=None, *, session: "Session"):
2216
+ """
2217
+ List all historical requests in a specific state from a source RSE to a destination RSE.
2218
+
2219
+ :param src_rse_ids: source RSE ids.
2220
+ :param dst_rse_ids: destination RSE ids.
2221
+ :param states: list of request states.
2222
+ :param offset: offset (for paging).
2223
+ :param limit: limit number of results.
2224
+ :param session: The database session in use.
2225
+ """
2226
+ if not states:
2227
+ states = [RequestState.WAITING]
2228
+
2229
+ stmt = select(
2230
+ models.RequestHistory
2231
+ ).filter(
2232
+ models.RequestHistory.state.in_(states),
2233
+ models.RequestHistory.source_rse_id.in_(src_rse_ids),
2234
+ models.RequestHistory.dest_rse_id.in_(dst_rse_ids)
2235
+ )
2236
+ if offset:
2237
+ stmt = stmt.offset(offset)
2238
+ if limit:
2239
+ stmt = stmt.limit(limit)
2240
+ for request in session.execute(stmt).yield_per(500).scalars():
2241
+ yield request