rucio 32.8.6__py3-none-any.whl → 35.8.0__py3-none-any.whl

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

Potentially problematic release.


This version of rucio might be problematic. Click here for more details.

Files changed (502) hide show
  1. rucio/__init__.py +0 -1
  2. rucio/alembicrevision.py +1 -2
  3. rucio/client/__init__.py +0 -1
  4. rucio/client/accountclient.py +45 -25
  5. rucio/client/accountlimitclient.py +37 -9
  6. rucio/client/baseclient.py +199 -154
  7. rucio/client/client.py +2 -3
  8. rucio/client/configclient.py +19 -6
  9. rucio/client/credentialclient.py +9 -4
  10. rucio/client/didclient.py +238 -63
  11. rucio/client/diracclient.py +13 -5
  12. rucio/client/downloadclient.py +162 -51
  13. rucio/client/exportclient.py +4 -4
  14. rucio/client/fileclient.py +3 -4
  15. rucio/client/importclient.py +4 -4
  16. rucio/client/lifetimeclient.py +21 -5
  17. rucio/client/lockclient.py +18 -8
  18. rucio/client/{metaclient.py → metaconventionsclient.py} +18 -15
  19. rucio/client/pingclient.py +0 -1
  20. rucio/client/replicaclient.py +15 -5
  21. rucio/client/requestclient.py +35 -19
  22. rucio/client/rseclient.py +133 -51
  23. rucio/client/ruleclient.py +29 -22
  24. rucio/client/scopeclient.py +8 -6
  25. rucio/client/subscriptionclient.py +47 -35
  26. rucio/client/touchclient.py +8 -4
  27. rucio/client/uploadclient.py +166 -82
  28. rucio/common/__init__.py +0 -1
  29. rucio/common/cache.py +4 -4
  30. rucio/common/config.py +52 -47
  31. rucio/common/constants.py +69 -2
  32. rucio/common/constraints.py +0 -1
  33. rucio/common/didtype.py +24 -22
  34. rucio/common/dumper/__init__.py +70 -41
  35. rucio/common/dumper/consistency.py +26 -22
  36. rucio/common/dumper/data_models.py +16 -23
  37. rucio/common/dumper/path_parsing.py +0 -1
  38. rucio/common/exception.py +281 -222
  39. rucio/common/extra.py +0 -1
  40. rucio/common/logging.py +54 -38
  41. rucio/common/pcache.py +122 -101
  42. rucio/common/plugins.py +153 -0
  43. rucio/common/policy.py +4 -4
  44. rucio/common/schema/__init__.py +17 -10
  45. rucio/common/schema/atlas.py +7 -5
  46. rucio/common/schema/belleii.py +7 -5
  47. rucio/common/schema/domatpc.py +7 -5
  48. rucio/common/schema/escape.py +7 -5
  49. rucio/common/schema/generic.py +8 -6
  50. rucio/common/schema/generic_multi_vo.py +7 -5
  51. rucio/common/schema/icecube.py +7 -5
  52. rucio/common/stomp_utils.py +0 -1
  53. rucio/common/stopwatch.py +0 -1
  54. rucio/common/test_rucio_server.py +2 -2
  55. rucio/common/types.py +262 -17
  56. rucio/common/utils.py +743 -451
  57. rucio/core/__init__.py +0 -1
  58. rucio/core/account.py +99 -29
  59. rucio/core/account_counter.py +89 -24
  60. rucio/core/account_limit.py +90 -24
  61. rucio/core/authentication.py +86 -29
  62. rucio/core/config.py +108 -38
  63. rucio/core/credential.py +14 -7
  64. rucio/core/did.py +680 -782
  65. rucio/core/did_meta_plugins/__init__.py +8 -6
  66. rucio/core/did_meta_plugins/did_column_meta.py +17 -12
  67. rucio/core/did_meta_plugins/did_meta_plugin_interface.py +60 -11
  68. rucio/core/did_meta_plugins/filter_engine.py +90 -50
  69. rucio/core/did_meta_plugins/json_meta.py +41 -16
  70. rucio/core/did_meta_plugins/mongo_meta.py +25 -8
  71. rucio/core/did_meta_plugins/postgres_meta.py +3 -4
  72. rucio/core/dirac.py +46 -17
  73. rucio/core/distance.py +66 -43
  74. rucio/core/exporter.py +5 -5
  75. rucio/core/heartbeat.py +181 -81
  76. rucio/core/identity.py +22 -12
  77. rucio/core/importer.py +23 -12
  78. rucio/core/lifetime_exception.py +32 -32
  79. rucio/core/lock.py +244 -142
  80. rucio/core/message.py +79 -38
  81. rucio/core/{meta.py → meta_conventions.py} +57 -44
  82. rucio/core/monitor.py +19 -13
  83. rucio/core/naming_convention.py +68 -27
  84. rucio/core/nongrid_trace.py +17 -5
  85. rucio/core/oidc.py +151 -29
  86. rucio/core/permission/__init__.py +18 -6
  87. rucio/core/permission/atlas.py +50 -35
  88. rucio/core/permission/belleii.py +6 -5
  89. rucio/core/permission/escape.py +8 -6
  90. rucio/core/permission/generic.py +82 -80
  91. rucio/core/permission/generic_multi_vo.py +9 -7
  92. rucio/core/quarantined_replica.py +91 -58
  93. rucio/core/replica.py +1303 -772
  94. rucio/core/replica_sorter.py +10 -12
  95. rucio/core/request.py +1133 -285
  96. rucio/core/rse.py +142 -102
  97. rucio/core/rse_counter.py +49 -18
  98. rucio/core/rse_expression_parser.py +6 -7
  99. rucio/core/rse_selector.py +41 -16
  100. rucio/core/rule.py +1538 -474
  101. rucio/core/rule_grouping.py +213 -68
  102. rucio/core/scope.py +50 -22
  103. rucio/core/subscription.py +92 -44
  104. rucio/core/topology.py +66 -24
  105. rucio/core/trace.py +42 -28
  106. rucio/core/transfer.py +543 -259
  107. rucio/core/vo.py +36 -18
  108. rucio/core/volatile_replica.py +59 -32
  109. rucio/daemons/__init__.py +0 -1
  110. rucio/daemons/abacus/__init__.py +0 -1
  111. rucio/daemons/abacus/account.py +29 -19
  112. rucio/daemons/abacus/collection_replica.py +21 -10
  113. rucio/daemons/abacus/rse.py +22 -12
  114. rucio/daemons/atropos/__init__.py +0 -1
  115. rucio/daemons/atropos/atropos.py +1 -2
  116. rucio/daemons/auditor/__init__.py +56 -28
  117. rucio/daemons/auditor/hdfs.py +17 -6
  118. rucio/daemons/auditor/srmdumps.py +116 -45
  119. rucio/daemons/automatix/__init__.py +0 -1
  120. rucio/daemons/automatix/automatix.py +30 -18
  121. rucio/daemons/badreplicas/__init__.py +0 -1
  122. rucio/daemons/badreplicas/minos.py +29 -18
  123. rucio/daemons/badreplicas/minos_temporary_expiration.py +5 -7
  124. rucio/daemons/badreplicas/necromancer.py +9 -13
  125. rucio/daemons/bb8/__init__.py +0 -1
  126. rucio/daemons/bb8/bb8.py +10 -13
  127. rucio/daemons/bb8/common.py +151 -154
  128. rucio/daemons/bb8/nuclei_background_rebalance.py +15 -9
  129. rucio/daemons/bb8/t2_background_rebalance.py +15 -8
  130. rucio/daemons/c3po/__init__.py +0 -1
  131. rucio/daemons/c3po/algorithms/__init__.py +0 -1
  132. rucio/daemons/c3po/algorithms/simple.py +8 -5
  133. rucio/daemons/c3po/algorithms/t2_free_space.py +10 -7
  134. rucio/daemons/c3po/algorithms/t2_free_space_only_pop.py +10 -7
  135. rucio/daemons/c3po/algorithms/t2_free_space_only_pop_with_network.py +30 -15
  136. rucio/daemons/c3po/c3po.py +81 -52
  137. rucio/daemons/c3po/collectors/__init__.py +0 -1
  138. rucio/daemons/c3po/collectors/agis.py +17 -17
  139. rucio/daemons/c3po/collectors/free_space.py +32 -13
  140. rucio/daemons/c3po/collectors/jedi_did.py +14 -5
  141. rucio/daemons/c3po/collectors/mock_did.py +11 -6
  142. rucio/daemons/c3po/collectors/network_metrics.py +12 -4
  143. rucio/daemons/c3po/collectors/workload.py +21 -19
  144. rucio/daemons/c3po/utils/__init__.py +0 -1
  145. rucio/daemons/c3po/utils/dataset_cache.py +15 -5
  146. rucio/daemons/c3po/utils/expiring_dataset_cache.py +16 -5
  147. rucio/daemons/c3po/utils/expiring_list.py +6 -7
  148. rucio/daemons/c3po/utils/popularity.py +5 -2
  149. rucio/daemons/c3po/utils/timeseries.py +25 -12
  150. rucio/daemons/cache/__init__.py +0 -1
  151. rucio/daemons/cache/consumer.py +21 -15
  152. rucio/daemons/common.py +42 -18
  153. rucio/daemons/conveyor/__init__.py +0 -1
  154. rucio/daemons/conveyor/common.py +69 -37
  155. rucio/daemons/conveyor/finisher.py +83 -46
  156. rucio/daemons/conveyor/poller.py +101 -69
  157. rucio/daemons/conveyor/preparer.py +35 -28
  158. rucio/daemons/conveyor/receiver.py +64 -21
  159. rucio/daemons/conveyor/stager.py +33 -28
  160. rucio/daemons/conveyor/submitter.py +71 -47
  161. rucio/daemons/conveyor/throttler.py +99 -35
  162. rucio/daemons/follower/__init__.py +0 -1
  163. rucio/daemons/follower/follower.py +12 -8
  164. rucio/daemons/hermes/__init__.py +0 -1
  165. rucio/daemons/hermes/hermes.py +57 -21
  166. rucio/daemons/judge/__init__.py +0 -1
  167. rucio/daemons/judge/cleaner.py +27 -17
  168. rucio/daemons/judge/evaluator.py +31 -18
  169. rucio/daemons/judge/injector.py +31 -23
  170. rucio/daemons/judge/repairer.py +28 -18
  171. rucio/daemons/oauthmanager/__init__.py +0 -1
  172. rucio/daemons/oauthmanager/oauthmanager.py +7 -8
  173. rucio/daemons/reaper/__init__.py +0 -1
  174. rucio/daemons/reaper/dark_reaper.py +15 -9
  175. rucio/daemons/reaper/reaper.py +109 -67
  176. rucio/daemons/replicarecoverer/__init__.py +0 -1
  177. rucio/daemons/replicarecoverer/suspicious_replica_recoverer.py +255 -116
  178. rucio/{api → daemons/rsedecommissioner}/__init__.py +0 -1
  179. rucio/daemons/rsedecommissioner/config.py +81 -0
  180. rucio/daemons/rsedecommissioner/profiles/__init__.py +24 -0
  181. rucio/daemons/rsedecommissioner/profiles/atlas.py +60 -0
  182. rucio/daemons/rsedecommissioner/profiles/generic.py +451 -0
  183. rucio/daemons/rsedecommissioner/profiles/types.py +92 -0
  184. rucio/daemons/rsedecommissioner/rse_decommissioner.py +280 -0
  185. rucio/daemons/storage/__init__.py +0 -1
  186. rucio/daemons/storage/consistency/__init__.py +0 -1
  187. rucio/daemons/storage/consistency/actions.py +152 -59
  188. rucio/daemons/tracer/__init__.py +0 -1
  189. rucio/daemons/tracer/kronos.py +47 -24
  190. rucio/daemons/transmogrifier/__init__.py +0 -1
  191. rucio/daemons/transmogrifier/transmogrifier.py +35 -26
  192. rucio/daemons/undertaker/__init__.py +0 -1
  193. rucio/daemons/undertaker/undertaker.py +10 -10
  194. rucio/db/__init__.py +0 -1
  195. rucio/db/sqla/__init__.py +16 -2
  196. rucio/db/sqla/constants.py +10 -1
  197. rucio/db/sqla/migrate_repo/__init__.py +0 -1
  198. rucio/db/sqla/migrate_repo/env.py +0 -1
  199. rucio/db/sqla/migrate_repo/versions/01eaf73ab656_add_new_rule_notification_state_progress.py +0 -1
  200. rucio/db/sqla/migrate_repo/versions/0437a40dbfd1_add_eol_at_in_rules.py +0 -3
  201. rucio/db/sqla/migrate_repo/versions/0f1adb7a599a_create_transfer_hops_table.py +1 -3
  202. rucio/db/sqla/migrate_repo/versions/102efcf145f4_added_stuck_at_column_to_rules.py +0 -3
  203. rucio/db/sqla/migrate_repo/versions/13d4f70c66a9_introduce_transfer_limits.py +1 -3
  204. rucio/db/sqla/migrate_repo/versions/140fef722e91_cleanup_distances_table.py +1 -3
  205. rucio/db/sqla/migrate_repo/versions/14ec5aeb64cf_add_request_external_host.py +0 -3
  206. rucio/db/sqla/migrate_repo/versions/156fb5b5a14_add_request_type_to_requests_idx.py +1 -4
  207. rucio/db/sqla/migrate_repo/versions/1677d4d803c8_split_rse_availability_into_multiple.py +0 -1
  208. rucio/db/sqla/migrate_repo/versions/16a0aca82e12_create_index_on_table_replicas_path.py +0 -2
  209. rucio/db/sqla/migrate_repo/versions/1803333ac20f_adding_provenance_and_phys_group.py +0 -1
  210. rucio/db/sqla/migrate_repo/versions/1a29d6a9504c_add_didtype_chck_to_requests.py +0 -1
  211. rucio/db/sqla/migrate_repo/versions/1a80adff031a_create_index_on_rules_hist_recent.py +0 -2
  212. rucio/db/sqla/migrate_repo/versions/1c45d9730ca6_increase_identity_length.py +0 -1
  213. rucio/db/sqla/migrate_repo/versions/1d1215494e95_add_quarantined_replicas_table.py +1 -3
  214. rucio/db/sqla/migrate_repo/versions/1d96f484df21_asynchronous_rules_and_rule_approval.py +0 -1
  215. rucio/db/sqla/migrate_repo/versions/1f46c5f240ac_add_bytes_column_to_bad_replicas.py +0 -3
  216. rucio/db/sqla/migrate_repo/versions/1fc15ab60d43_add_message_history_table.py +0 -1
  217. rucio/db/sqla/migrate_repo/versions/2190e703eb6e_move_rse_settings_to_rse_attributes.py +1 -2
  218. rucio/db/sqla/migrate_repo/versions/21d6b9dc9961_add_mismatch_scheme_state_to_requests.py +0 -1
  219. rucio/db/sqla/migrate_repo/versions/22cf51430c78_add_availability_column_to_table_rses.py +0 -3
  220. rucio/db/sqla/migrate_repo/versions/22d887e4ec0a_create_sources_table.py +1 -3
  221. rucio/db/sqla/migrate_repo/versions/25821a8a45a3_remove_unique_constraint_on_requests.py +1 -4
  222. rucio/db/sqla/migrate_repo/versions/25fc855625cf_added_unique_constraint_to_rules.py +0 -2
  223. rucio/db/sqla/migrate_repo/versions/269fee20dee9_add_repair_cnt_to_locks.py +0 -3
  224. rucio/db/sqla/migrate_repo/versions/271a46ea6244_add_ignore_availability_column_to_rules.py +0 -3
  225. rucio/db/sqla/migrate_repo/versions/277b5fbb41d3_switch_heartbeats_executable.py +1 -2
  226. rucio/db/sqla/migrate_repo/versions/27e3a68927fb_remove_replicas_tombstone_and_replicas_.py +0 -1
  227. rucio/db/sqla/migrate_repo/versions/2854cd9e168_added_rule_id_column.py +0 -1
  228. rucio/db/sqla/migrate_repo/versions/295289b5a800_processed_by_and__at_in_requests.py +0 -2
  229. rucio/db/sqla/migrate_repo/versions/2962ece31cf4_add_nbaccesses_column_in_the_did_table.py +0 -3
  230. rucio/db/sqla/migrate_repo/versions/2af3291ec4c_added_replicas_history_table.py +1 -3
  231. rucio/db/sqla/migrate_repo/versions/2b69addda658_add_columns_for_third_party_copy_read_.py +0 -2
  232. rucio/db/sqla/migrate_repo/versions/2b8e7bcb4783_add_config_table.py +1 -4
  233. rucio/db/sqla/migrate_repo/versions/2ba5229cb54c_add_submitted_at_to_requests_table.py +0 -3
  234. rucio/db/sqla/migrate_repo/versions/2cbee484dcf9_added_column_volume_to_rse_transfer_.py +1 -4
  235. rucio/db/sqla/migrate_repo/versions/2edee4a83846_add_source_to_requests_and_requests_.py +0 -1
  236. rucio/db/sqla/migrate_repo/versions/2eef46be23d4_change_tokens_pk.py +1 -3
  237. rucio/db/sqla/migrate_repo/versions/2f648fc909f3_index_in_rule_history_on_scope_name.py +0 -2
  238. rucio/db/sqla/migrate_repo/versions/3082b8cef557_add_naming_convention_table_and_closed_.py +1 -3
  239. rucio/db/sqla/migrate_repo/versions/30fa38b6434e_add_index_on_service_column_in_the_message_table.py +1 -3
  240. rucio/db/sqla/migrate_repo/versions/3152492b110b_added_staging_area_column.py +1 -2
  241. rucio/db/sqla/migrate_repo/versions/32c7d2783f7e_create_bad_replicas_table.py +1 -3
  242. rucio/db/sqla/migrate_repo/versions/3345511706b8_replicas_table_pk_definition_is_in_.py +1 -3
  243. rucio/db/sqla/migrate_repo/versions/35ef10d1e11b_change_index_on_table_requests.py +0 -2
  244. rucio/db/sqla/migrate_repo/versions/379a19b5332d_create_rse_limits_table.py +1 -3
  245. rucio/db/sqla/migrate_repo/versions/384b96aa0f60_created_rule_history_tables.py +2 -3
  246. rucio/db/sqla/migrate_repo/versions/3ac1660a1a72_extend_distance_table.py +0 -3
  247. rucio/db/sqla/migrate_repo/versions/3ad36e2268b0_create_collection_replicas_updates_table.py +1 -4
  248. rucio/db/sqla/migrate_repo/versions/3c9df354071b_extend_waiting_request_state.py +0 -1
  249. rucio/db/sqla/migrate_repo/versions/3d9813fab443_add_a_new_state_lost_in_badfilesstatus.py +0 -1
  250. rucio/db/sqla/migrate_repo/versions/40ad39ce3160_add_transferred_at_to_requests_table.py +0 -3
  251. rucio/db/sqla/migrate_repo/versions/4207be2fd914_add_notification_column_to_rules.py +0 -1
  252. rucio/db/sqla/migrate_repo/versions/42db2617c364_create_index_on_requests_external_id.py +0 -2
  253. rucio/db/sqla/migrate_repo/versions/436827b13f82_added_column_activity_to_table_requests.py +0 -3
  254. rucio/db/sqla/migrate_repo/versions/44278720f774_update_requests_typ_sta_upd_idx_index.py +0 -2
  255. rucio/db/sqla/migrate_repo/versions/45378a1e76a8_create_collection_replica_table.py +2 -4
  256. rucio/db/sqla/migrate_repo/versions/469d262be19_removing_created_at_index.py +0 -2
  257. rucio/db/sqla/migrate_repo/versions/4783c1f49cb4_create_distance_table.py +1 -3
  258. rucio/db/sqla/migrate_repo/versions/49a21b4d4357_create_index_on_table_tokens.py +1 -4
  259. rucio/db/sqla/migrate_repo/versions/4a2cbedda8b9_add_source_replica_expression_column_to_.py +0 -3
  260. rucio/db/sqla/migrate_repo/versions/4a7182d9578b_added_bytes_length_accessed_at_columns.py +0 -3
  261. rucio/db/sqla/migrate_repo/versions/4bab9edd01fc_create_index_on_requests_rule_id.py +0 -2
  262. rucio/db/sqla/migrate_repo/versions/4c3a4acfe006_new_attr_account_table.py +1 -3
  263. rucio/db/sqla/migrate_repo/versions/4cf0a2e127d4_adding_transient_metadata.py +0 -3
  264. rucio/db/sqla/migrate_repo/versions/4df2c5ddabc0_remove_temporary_dids.py +55 -0
  265. rucio/db/sqla/migrate_repo/versions/50280c53117c_add_qos_class_to_rse.py +0 -2
  266. rucio/db/sqla/migrate_repo/versions/52153819589c_add_rse_id_to_replicas_table.py +0 -2
  267. rucio/db/sqla/migrate_repo/versions/52fd9f4916fa_added_activity_to_rules.py +0 -3
  268. rucio/db/sqla/migrate_repo/versions/53b479c3cb0f_fix_did_meta_table_missing_updated_at_.py +0 -3
  269. rucio/db/sqla/migrate_repo/versions/5673b4b6e843_add_wfms_metadata_to_rule_tables.py +0 -3
  270. rucio/db/sqla/migrate_repo/versions/575767d9f89_added_source_history_table.py +1 -2
  271. rucio/db/sqla/migrate_repo/versions/58bff7008037_add_started_at_to_requests.py +0 -3
  272. rucio/db/sqla/migrate_repo/versions/58c8b78301ab_rename_callback_to_message.py +1 -3
  273. rucio/db/sqla/migrate_repo/versions/5f139f77382a_added_child_rule_id_column.py +1 -3
  274. rucio/db/sqla/migrate_repo/versions/688ef1840840_adding_did_meta_table.py +1 -2
  275. rucio/db/sqla/migrate_repo/versions/6e572a9bfbf3_add_new_split_container_column_to_rules.py +0 -3
  276. rucio/db/sqla/migrate_repo/versions/70587619328_add_comment_column_for_subscriptions.py +0 -3
  277. rucio/db/sqla/migrate_repo/versions/739064d31565_remove_history_table_pks.py +1 -2
  278. rucio/db/sqla/migrate_repo/versions/7541902bf173_add_didsfollowed_and_followevents_table.py +2 -4
  279. rucio/db/sqla/migrate_repo/versions/7ec22226cdbf_new_replica_state_for_temporary_.py +0 -1
  280. rucio/db/sqla/migrate_repo/versions/810a41685bc1_added_columns_rse_transfer_limits.py +1 -4
  281. rucio/db/sqla/migrate_repo/versions/83f991c63a93_correct_rse_expression_length.py +0 -2
  282. rucio/db/sqla/migrate_repo/versions/8523998e2e76_increase_size_of_extended_attributes_.py +0 -3
  283. rucio/db/sqla/migrate_repo/versions/8ea9122275b1_adding_missing_function_based_indices.py +1 -2
  284. rucio/db/sqla/migrate_repo/versions/90f47792bb76_add_clob_payload_to_messages.py +0 -3
  285. rucio/db/sqla/migrate_repo/versions/914b8f02df38_new_table_for_lifetime_model_exceptions.py +1 -3
  286. rucio/db/sqla/migrate_repo/versions/94a5961ddbf2_add_estimator_columns.py +0 -3
  287. rucio/db/sqla/migrate_repo/versions/9a1b149a2044_add_saml_identity_type.py +0 -1
  288. rucio/db/sqla/migrate_repo/versions/9a45bc4ea66d_add_vp_table.py +1 -2
  289. rucio/db/sqla/migrate_repo/versions/9eb936a81eb1_true_is_true.py +0 -2
  290. rucio/db/sqla/migrate_repo/versions/a08fa8de1545_transfer_stats_table.py +55 -0
  291. rucio/db/sqla/migrate_repo/versions/a118956323f8_added_vo_table_and_vo_col_to_rse.py +1 -3
  292. rucio/db/sqla/migrate_repo/versions/a193a275255c_add_status_column_in_messages.py +0 -2
  293. rucio/db/sqla/migrate_repo/versions/a5f6f6e928a7_1_7_0.py +1 -4
  294. rucio/db/sqla/migrate_repo/versions/a616581ee47_added_columns_to_table_requests.py +0 -1
  295. rucio/db/sqla/migrate_repo/versions/a6eb23955c28_state_idx_non_functional.py +0 -1
  296. rucio/db/sqla/migrate_repo/versions/a74275a1ad30_added_global_quota_table.py +1 -3
  297. rucio/db/sqla/migrate_repo/versions/a93e4e47bda_heartbeats.py +1 -4
  298. rucio/db/sqla/migrate_repo/versions/ae2a56fcc89_added_comment_column_to_rules.py +0 -1
  299. rucio/db/sqla/migrate_repo/versions/b0070f3695c8_add_deletedidmeta_table.py +57 -0
  300. rucio/db/sqla/migrate_repo/versions/b4293a99f344_added_column_identity_to_table_tokens.py +0 -3
  301. rucio/db/sqla/migrate_repo/versions/b5493606bbf5_fix_primary_key_for_subscription_history.py +41 -0
  302. rucio/db/sqla/migrate_repo/versions/b7d287de34fd_removal_of_replicastate_source.py +1 -2
  303. rucio/db/sqla/migrate_repo/versions/b818052fa670_add_index_to_quarantined_replicas.py +1 -3
  304. rucio/db/sqla/migrate_repo/versions/b8caac94d7f0_add_comments_column_for_subscriptions_.py +0 -3
  305. rucio/db/sqla/migrate_repo/versions/b96a1c7e1cc4_new_bad_pfns_table_and_bad_replicas_.py +1 -5
  306. rucio/db/sqla/migrate_repo/versions/bb695f45c04_extend_request_state.py +1 -3
  307. rucio/db/sqla/migrate_repo/versions/bc68e9946deb_add_staging_timestamps_to_request.py +0 -3
  308. rucio/db/sqla/migrate_repo/versions/bf3baa1c1474_correct_pk_and_idx_for_history_tables.py +1 -3
  309. rucio/db/sqla/migrate_repo/versions/c0937668555f_add_qos_policy_map_table.py +1 -2
  310. rucio/db/sqla/migrate_repo/versions/c129ccdb2d5_add_lumiblocknr_to_dids.py +0 -3
  311. rucio/db/sqla/migrate_repo/versions/ccdbcd48206e_add_did_type_column_index_on_did_meta_.py +1 -4
  312. rucio/db/sqla/migrate_repo/versions/cebad904c4dd_new_payload_column_for_heartbeats.py +1 -2
  313. rucio/db/sqla/migrate_repo/versions/d1189a09c6e0_oauth2_0_and_jwt_feature_support_adding_.py +1 -4
  314. rucio/db/sqla/migrate_repo/versions/d23453595260_extend_request_state_for_preparer.py +1 -3
  315. rucio/db/sqla/migrate_repo/versions/d6dceb1de2d_added_purge_column_to_rules.py +1 -4
  316. rucio/db/sqla/migrate_repo/versions/d6e2c3b2cf26_remove_third_party_copy_column_from_rse.py +0 -2
  317. rucio/db/sqla/migrate_repo/versions/d91002c5841_new_account_limits_table.py +1 -3
  318. rucio/db/sqla/migrate_repo/versions/e138c364ebd0_extending_columns_for_filter_and_.py +0 -3
  319. rucio/db/sqla/migrate_repo/versions/e59300c8b179_support_for_archive.py +1 -3
  320. rucio/db/sqla/migrate_repo/versions/f1b14a8c2ac1_postgres_use_check_constraints.py +0 -1
  321. rucio/db/sqla/migrate_repo/versions/f41ffe206f37_oracle_global_temporary_tables.py +1 -2
  322. rucio/db/sqla/migrate_repo/versions/f85a2962b021_adding_transfertool_column_to_requests_.py +1 -3
  323. rucio/db/sqla/migrate_repo/versions/fa7a7d78b602_increase_refresh_token_size.py +0 -2
  324. rucio/db/sqla/migrate_repo/versions/fb28a95fe288_add_replicas_rse_id_tombstone_idx.py +0 -1
  325. rucio/db/sqla/migrate_repo/versions/fe1a65b176c9_set_third_party_copy_read_and_write_.py +1 -2
  326. rucio/db/sqla/migrate_repo/versions/fe8ea2fa9788_added_third_party_copy_column_to_rse_.py +0 -3
  327. rucio/db/sqla/models.py +122 -216
  328. rucio/db/sqla/sautils.py +12 -5
  329. rucio/db/sqla/session.py +71 -43
  330. rucio/db/sqla/types.py +3 -4
  331. rucio/db/sqla/util.py +91 -69
  332. rucio/gateway/__init__.py +13 -0
  333. rucio/{api → gateway}/account.py +119 -46
  334. rucio/{api → gateway}/account_limit.py +12 -13
  335. rucio/{api → gateway}/authentication.py +106 -33
  336. rucio/{api → gateway}/config.py +12 -13
  337. rucio/{api → gateway}/credential.py +15 -4
  338. rucio/{api → gateway}/did.py +384 -140
  339. rucio/{api → gateway}/dirac.py +16 -6
  340. rucio/{api → gateway}/exporter.py +3 -4
  341. rucio/{api → gateway}/heartbeat.py +17 -5
  342. rucio/{api → gateway}/identity.py +63 -19
  343. rucio/{api → gateway}/importer.py +3 -4
  344. rucio/{api → gateway}/lifetime_exception.py +35 -10
  345. rucio/{api → gateway}/lock.py +34 -12
  346. rucio/{api/meta.py → gateway/meta_conventions.py} +18 -16
  347. rucio/{api → gateway}/permission.py +4 -5
  348. rucio/{api → gateway}/quarantined_replica.py +13 -4
  349. rucio/{api → gateway}/replica.py +12 -11
  350. rucio/{api → gateway}/request.py +129 -28
  351. rucio/{api → gateway}/rse.py +11 -12
  352. rucio/{api → gateway}/rule.py +117 -35
  353. rucio/{api → gateway}/scope.py +24 -14
  354. rucio/{api → gateway}/subscription.py +65 -43
  355. rucio/{api → gateway}/vo.py +17 -7
  356. rucio/rse/__init__.py +3 -4
  357. rucio/rse/protocols/__init__.py +0 -1
  358. rucio/rse/protocols/bittorrent.py +184 -0
  359. rucio/rse/protocols/cache.py +1 -2
  360. rucio/rse/protocols/dummy.py +1 -2
  361. rucio/rse/protocols/gfal.py +12 -10
  362. rucio/rse/protocols/globus.py +7 -7
  363. rucio/rse/protocols/gsiftp.py +2 -3
  364. rucio/rse/protocols/http_cache.py +1 -2
  365. rucio/rse/protocols/mock.py +1 -2
  366. rucio/rse/protocols/ngarc.py +1 -2
  367. rucio/rse/protocols/posix.py +12 -13
  368. rucio/rse/protocols/protocol.py +116 -52
  369. rucio/rse/protocols/rclone.py +6 -7
  370. rucio/rse/protocols/rfio.py +4 -5
  371. rucio/rse/protocols/srm.py +9 -10
  372. rucio/rse/protocols/ssh.py +8 -9
  373. rucio/rse/protocols/storm.py +2 -3
  374. rucio/rse/protocols/webdav.py +17 -14
  375. rucio/rse/protocols/xrootd.py +23 -17
  376. rucio/rse/rsemanager.py +19 -7
  377. rucio/tests/__init__.py +0 -1
  378. rucio/tests/common.py +43 -17
  379. rucio/tests/common_server.py +3 -3
  380. rucio/transfertool/__init__.py +0 -1
  381. rucio/transfertool/bittorrent.py +199 -0
  382. rucio/transfertool/bittorrent_driver.py +52 -0
  383. rucio/transfertool/bittorrent_driver_qbittorrent.py +133 -0
  384. rucio/transfertool/fts3.py +250 -138
  385. rucio/transfertool/fts3_plugins.py +152 -0
  386. rucio/transfertool/globus.py +9 -8
  387. rucio/transfertool/globus_library.py +1 -2
  388. rucio/transfertool/mock.py +21 -12
  389. rucio/transfertool/transfertool.py +33 -24
  390. rucio/vcsversion.py +4 -4
  391. rucio/version.py +5 -13
  392. rucio/web/__init__.py +0 -1
  393. rucio/web/rest/__init__.py +0 -1
  394. rucio/web/rest/flaskapi/__init__.py +0 -1
  395. rucio/web/rest/flaskapi/authenticated_bp.py +0 -1
  396. rucio/web/rest/flaskapi/v1/__init__.py +0 -1
  397. rucio/web/rest/flaskapi/v1/accountlimits.py +15 -13
  398. rucio/web/rest/flaskapi/v1/accounts.py +49 -48
  399. rucio/web/rest/flaskapi/v1/archives.py +12 -10
  400. rucio/web/rest/flaskapi/v1/auth.py +146 -144
  401. rucio/web/rest/flaskapi/v1/common.py +82 -41
  402. rucio/web/rest/flaskapi/v1/config.py +5 -6
  403. rucio/web/rest/flaskapi/v1/credentials.py +7 -8
  404. rucio/web/rest/flaskapi/v1/dids.py +158 -28
  405. rucio/web/rest/flaskapi/v1/dirac.py +8 -8
  406. rucio/web/rest/flaskapi/v1/export.py +3 -5
  407. rucio/web/rest/flaskapi/v1/heartbeats.py +3 -5
  408. rucio/web/rest/flaskapi/v1/identities.py +3 -5
  409. rucio/web/rest/flaskapi/v1/import.py +3 -4
  410. rucio/web/rest/flaskapi/v1/lifetime_exceptions.py +6 -9
  411. rucio/web/rest/flaskapi/v1/locks.py +2 -4
  412. rucio/web/rest/flaskapi/v1/main.py +10 -2
  413. rucio/web/rest/flaskapi/v1/{meta.py → meta_conventions.py} +26 -11
  414. rucio/web/rest/flaskapi/v1/metrics.py +1 -2
  415. rucio/web/rest/flaskapi/v1/nongrid_traces.py +4 -4
  416. rucio/web/rest/flaskapi/v1/ping.py +6 -7
  417. rucio/web/rest/flaskapi/v1/redirect.py +8 -9
  418. rucio/web/rest/flaskapi/v1/replicas.py +43 -19
  419. rucio/web/rest/flaskapi/v1/requests.py +178 -21
  420. rucio/web/rest/flaskapi/v1/rses.py +61 -26
  421. rucio/web/rest/flaskapi/v1/rules.py +48 -18
  422. rucio/web/rest/flaskapi/v1/scopes.py +3 -5
  423. rucio/web/rest/flaskapi/v1/subscriptions.py +22 -18
  424. rucio/web/rest/flaskapi/v1/traces.py +4 -4
  425. rucio/web/rest/flaskapi/v1/types.py +20 -0
  426. rucio/web/rest/flaskapi/v1/vos.py +3 -5
  427. rucio/web/rest/main.py +0 -1
  428. rucio/web/rest/metrics.py +0 -1
  429. rucio/web/rest/ping.py +27 -0
  430. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/ldap.cfg.template +1 -1
  431. rucio-35.8.0.data/data/rucio/requirements.server.txt +268 -0
  432. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/tools/bootstrap.py +3 -3
  433. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/tools/merge_rucio_configs.py +2 -5
  434. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/tools/reset_database.py +3 -3
  435. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio +87 -85
  436. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-abacus-account +0 -1
  437. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-abacus-collection-replica +0 -1
  438. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-abacus-rse +0 -1
  439. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-admin +45 -32
  440. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-atropos +0 -1
  441. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-auditor +13 -7
  442. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-automatix +1 -2
  443. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-bb8 +0 -1
  444. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-c3po +0 -1
  445. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-cache-client +2 -3
  446. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-cache-consumer +0 -1
  447. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-finisher +1 -2
  448. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-poller +0 -1
  449. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-preparer +0 -1
  450. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-receiver +0 -1
  451. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-stager +0 -1
  452. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-submitter +2 -3
  453. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-throttler +0 -1
  454. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-dark-reaper +0 -1
  455. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-dumper +11 -10
  456. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-follower +0 -1
  457. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-hermes +0 -1
  458. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-judge-cleaner +0 -1
  459. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-judge-evaluator +2 -3
  460. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-judge-injector +0 -1
  461. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-judge-repairer +0 -1
  462. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-kronos +1 -3
  463. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-minos +0 -1
  464. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-minos-temporary-expiration +0 -1
  465. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-necromancer +1 -2
  466. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-oauth-manager +2 -3
  467. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-reaper +0 -1
  468. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-replica-recoverer +6 -7
  469. rucio-35.8.0.data/scripts/rucio-rse-decommissioner +66 -0
  470. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-storage-consistency-actions +0 -1
  471. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-transmogrifier +0 -1
  472. {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-undertaker +1 -2
  473. rucio-35.8.0.dist-info/METADATA +72 -0
  474. rucio-35.8.0.dist-info/RECORD +493 -0
  475. {rucio-32.8.6.dist-info → rucio-35.8.0.dist-info}/WHEEL +1 -1
  476. {rucio-32.8.6.dist-info → rucio-35.8.0.dist-info}/licenses/AUTHORS.rst +3 -0
  477. rucio/api/temporary_did.py +0 -49
  478. rucio/common/schema/cms.py +0 -478
  479. rucio/common/schema/lsst.py +0 -423
  480. rucio/core/permission/cms.py +0 -1166
  481. rucio/core/temporary_did.py +0 -188
  482. rucio/daemons/reaper/light_reaper.py +0 -255
  483. rucio/web/rest/flaskapi/v1/tmp_dids.py +0 -115
  484. rucio-32.8.6.data/data/rucio/requirements.txt +0 -55
  485. rucio-32.8.6.data/scripts/rucio-light-reaper +0 -53
  486. rucio-32.8.6.dist-info/METADATA +0 -83
  487. rucio-32.8.6.dist-info/RECORD +0 -481
  488. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/alembic.ini.template +0 -0
  489. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/alembic_offline.ini.template +0 -0
  490. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/globus-config.yml.template +0 -0
  491. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/mail_templates/rule_approval_request.tmpl +0 -0
  492. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +0 -0
  493. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/mail_templates/rule_approved_user.tmpl +0 -0
  494. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +0 -0
  495. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/mail_templates/rule_denied_user.tmpl +0 -0
  496. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +0 -0
  497. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/rse-accounts.cfg.template +0 -0
  498. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/rucio.cfg.atlas.client.template +0 -0
  499. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/rucio.cfg.template +0 -0
  500. {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/rucio_multi_vo.cfg.template +0 -0
  501. {rucio-32.8.6.dist-info → rucio-35.8.0.dist-info}/licenses/LICENSE +0 -0
  502. {rucio-32.8.6.dist-info → rucio-35.8.0.dist-info}/top_level.txt +0 -0
rucio/core/request.py CHANGED
@@ -1,4 +1,3 @@
1
- # -*- coding: utf-8 -*-
2
1
  # Copyright European Organization for Nuclear Research (CERN) since 2012
3
2
  #
4
3
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,38 +13,50 @@
14
13
  # limitations under the License.
15
14
 
16
15
  import datetime
16
+ import itertools
17
17
  import json
18
18
  import logging
19
+ import math
20
+ import random
21
+ import threading
19
22
  import traceback
20
23
  import uuid
21
- from collections import namedtuple
22
- from collections.abc import Sequence
24
+ from abc import ABCMeta, abstractmethod
25
+ from collections import defaultdict, namedtuple
26
+ from collections.abc import Iterable, Iterator, Mapping, Sequence
27
+ from dataclasses import dataclass
23
28
  from typing import TYPE_CHECKING, Any, Optional, Union
24
29
 
25
- from sqlalchemy import and_, or_, update, select, delete, exists, insert
30
+ from sqlalchemy import and_, delete, exists, insert, or_, select, update
26
31
  from sqlalchemy.exc import IntegrityError
27
32
  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
33
+ from sqlalchemy.sql.expression import asc, false, func, null, true
34
+ from sqlalchemy.sql.functions import coalesce
35
+
36
+ from rucio.common.config import config_get_bool, config_get_int
37
+ from rucio.common.constants import RseAttr
38
+ from rucio.common.exception import InvalidRSEExpression, RequestNotFound, RucioException, UnsupportedOperation
39
+ from rucio.common.types import FilterDict, InternalAccount, InternalScope, LoggerFunction, RequestDict
40
+ from rucio.common.utils import chunks, generate_uuid
41
+ from rucio.core.distance import get_distances
34
42
  from rucio.core.message import add_message, add_messages
35
43
  from rucio.core.monitor import MetricManager
36
- from rucio.core.rse import get_rse_attribute, get_rse_name, get_rse_vo, RseData
44
+ from rucio.core.rse import RseCollection, RseData, get_rse_attribute, get_rse_name, get_rse_vo
37
45
  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
46
+ from rucio.db.sqla import filter_thread_work, models
47
+ from rucio.db.sqla.constants import LockState, ReplicaState, RequestErrMsg, RequestState, RequestType, TransferLimitDirection
48
+ from rucio.db.sqla.session import read_session, stream_session, transactional_session
41
49
  from rucio.db.sqla.util import temp_table_mngr
42
50
 
43
51
  RequestAndState = namedtuple('RequestAndState', ['request_id', 'request_state'])
44
52
 
45
53
  if TYPE_CHECKING:
46
- from rucio.core.rse import RseCollection
47
54
 
55
+ from sqlalchemy.engine import Row
48
56
  from sqlalchemy.orm import Session
57
+ from sqlalchemy.sql.selectable import Subquery
58
+
59
+ from rucio.rse.protocols.protocol import RSEProtocol
49
60
 
50
61
  """
51
62
  The core request.py is specifically for handling requests.
@@ -54,20 +65,48 @@ Requests accessed by external_id (So called transfers), are covered in the core
54
65
 
55
66
  METRICS = MetricManager(module=__name__)
56
67
 
68
+ TRANSFER_TIME_BUCKETS = (
69
+ 10, 30, 60, 5 * 60, 10 * 60, 20 * 60, 40 * 60, 60 * 60, 1.5 * 60 * 60, 3 * 60 * 60, 6 * 60 * 60,
70
+ 12 * 60 * 60, 24 * 60 * 60, 3 * 24 * 60 * 60, 4 * 24 * 60 * 60, 5 * 24 * 60 * 60,
71
+ 6 * 24 * 60 * 60, 7 * 24 * 60 * 60, 10 * 24 * 60 * 60, 14 * 24 * 60 * 60, 30 * 24 * 60 * 60,
72
+ float('inf')
73
+ )
74
+
57
75
 
58
76
  class RequestSource:
59
- def __init__(self, rse_data, ranking=None, distance=None, file_path=None, scheme=None, url=None):
60
- self.rse = rse_data
77
+ def __init__(
78
+ self,
79
+ rse: RseData,
80
+ ranking: Optional[int] = None,
81
+ distance: Optional[int] = None,
82
+ file_path: Optional[str] = None,
83
+ scheme: Optional[str] = None,
84
+ url: Optional[str] = None
85
+ ):
86
+ self.rse = rse
61
87
  self.distance = distance if distance is not None else 9999
62
88
  self.ranking = ranking if ranking is not None else 0
63
89
  self.file_path = file_path
64
90
  self.scheme = scheme
65
91
  self.url = url
66
92
 
67
- def __str__(self):
93
+ def __str__(self) -> str:
68
94
  return "src_rse={}".format(self.rse)
69
95
 
70
96
 
97
+ class TransferDestination:
98
+ def __init__(
99
+ self,
100
+ rse: RseData,
101
+ scheme: str
102
+ ):
103
+ self.rse = rse
104
+ self.scheme = scheme
105
+
106
+ def __str__(self) -> str:
107
+ return "dst_rse={}".format(self.rse)
108
+
109
+
71
110
  class RequestWithSources:
72
111
  def __init__(
73
112
  self,
@@ -82,14 +121,13 @@ class RequestWithSources:
82
121
  activity: str,
83
122
  attributes: Optional[Union[str, dict[str, Any]]],
84
123
  previous_attempt_id: Optional[str],
85
- dest_rse_data: RseData,
124
+ dest_rse: RseData,
86
125
  account: InternalAccount,
87
126
  retry_count: int,
88
127
  priority: int,
89
128
  transfertool: str,
90
129
  requested_at: Optional[datetime.datetime] = None,
91
130
  ):
92
-
93
131
  self.request_id = id_
94
132
  self.request_type = request_type
95
133
  self.rule_id = rule_id
@@ -102,7 +140,7 @@ class RequestWithSources:
102
140
  self._dict_attributes = None
103
141
  self._db_attributes = attributes
104
142
  self.previous_attempt_id = previous_attempt_id
105
- self.dest_rse = dest_rse_data
143
+ self.dest_rse = dest_rse
106
144
  self.account = account
107
145
  self.retry_count = retry_count or 0
108
146
  self.priority = priority if priority is not None else 3
@@ -112,17 +150,21 @@ class RequestWithSources:
112
150
  self.sources: list[RequestSource] = []
113
151
  self.requested_source: Optional[RequestSource] = None
114
152
 
115
- def __str__(self):
153
+ def __str__(self) -> str:
116
154
  return "{}({}:{})".format(self.request_id, self.scope, self.name)
117
155
 
118
156
  @property
119
- def attributes(self):
157
+ def attributes(self) -> dict[str, Any]:
120
158
  if self._dict_attributes is None:
121
- self.attributes = self._db_attributes
159
+ self._dict_attributes = self._parse_db_attributes(self._db_attributes)
122
160
  return self._dict_attributes
123
161
 
124
162
  @attributes.setter
125
- def attributes(self, db_attributes):
163
+ def attributes(self, db_attributes: dict[str, Any]) -> None:
164
+ self._dict_attributes = self._parse_db_attributes(db_attributes)
165
+
166
+ @staticmethod
167
+ def _parse_db_attributes(db_attributes: Optional[Union[str, dict[str, Any]]]) -> dict[str, Any]:
126
168
  attr = {}
127
169
  if db_attributes:
128
170
  if isinstance(db_attributes, dict):
@@ -134,10 +176,50 @@ class RequestWithSources:
134
176
  attr['allow_tape_source'] = attr["allow_tape_source"] if (attr and "allow_tape_source" in attr) else True
135
177
  attr['dsn'] = attr["ds_name"] if (attr and "ds_name" in attr) else None
136
178
  attr['lifetime'] = attr.get('lifetime', -1)
137
- self._dict_attributes = attr
179
+ return attr
180
+
181
+
182
+ class DirectTransfer(metaclass=ABCMeta):
183
+ """
184
+ The configuration for a direct (non-multi-hop) transfer. It can be a multi-source transfer.
185
+ """
186
+
187
+ def __init__(self, sources: list[RequestSource], rws: RequestWithSources) -> None:
188
+ self.sources: list[RequestSource] = sources
189
+ self.rws: RequestWithSources = rws
190
+
191
+ @property
192
+ @abstractmethod
193
+ def src(self) -> RequestSource:
194
+ pass
195
+
196
+ @property
197
+ @abstractmethod
198
+ def dst(self) -> TransferDestination:
199
+ pass
200
+
201
+ @property
202
+ @abstractmethod
203
+ def dest_url(self) -> str:
204
+ pass
205
+
206
+ @abstractmethod
207
+ def source_url(self, source: RequestSource) -> str:
208
+ pass
138
209
 
210
+ @abstractmethod
211
+ def dest_protocol(self) -> "RSEProtocol":
212
+ pass
139
213
 
140
- def should_retry_request(req, retry_protocol_mismatches):
214
+ @abstractmethod
215
+ def source_protocol(self, source: RequestSource) -> "RSEProtocol":
216
+ pass
217
+
218
+
219
+ def should_retry_request(
220
+ req: RequestDict,
221
+ retry_protocol_mismatches: bool
222
+ ) -> bool:
141
223
  """
142
224
  Whether should retry this request.
143
225
 
@@ -163,7 +245,14 @@ def should_retry_request(req, retry_protocol_mismatches):
163
245
 
164
246
  @METRICS.time_it
165
247
  @transactional_session
166
- def requeue_and_archive(request, source_ranking_update=True, retry_protocol_mismatches=False, *, session: "Session", logger=logging.log):
248
+ def requeue_and_archive(
249
+ request: RequestDict,
250
+ source_ranking_update: bool = True,
251
+ retry_protocol_mismatches: bool = False,
252
+ *,
253
+ session: "Session",
254
+ logger: LoggerFunction = logging.log
255
+ ) -> Optional[RequestDict]:
167
256
  """
168
257
  Requeue and archive a failed request.
169
258
  TODO: Multiple requeue.
@@ -208,7 +297,12 @@ def requeue_and_archive(request, source_ranking_update=True, retry_protocol_mism
208
297
 
209
298
  @METRICS.count_it
210
299
  @transactional_session
211
- def queue_requests(requests, *, session: "Session", logger=logging.log):
300
+ def queue_requests(
301
+ requests: Iterable[RequestDict],
302
+ *,
303
+ session: "Session",
304
+ logger: LoggerFunction = logging.log
305
+ ) -> list[str]:
212
306
  """
213
307
  Submit transfer requests on destination RSEs for data identifiers.
214
308
 
@@ -247,7 +341,9 @@ def queue_requests(requests, *, session: "Session", logger=logging.log):
247
341
  models.Request.name,
248
342
  models.Request.dest_rse_id
249
343
  ).with_hint(
250
- models.Request, "INDEX(REQUESTS REQUESTS_SC_NA_RS_TY_UQ_IDX)", 'oracle'
344
+ models.Request,
345
+ 'INDEX(REQUESTS REQUESTS_SC_NA_RS_TY_UQ_IDX)',
346
+ 'oracle'
251
347
  ).where(
252
348
  or_(*requests_condition)
253
349
  )
@@ -334,10 +430,16 @@ def queue_requests(requests, *, session: "Session", logger=logging.log):
334
430
  'payload': payload})
335
431
 
336
432
  for requests_chunk in chunks(new_requests, 1000):
337
- session.execute(insert(models.Request), requests_chunk)
433
+ stmt = insert(
434
+ models.Request
435
+ )
436
+ session.execute(stmt, requests_chunk)
338
437
 
339
438
  for sources_chunk in chunks(sources, 1000):
340
- session.execute(insert(models.Source), sources_chunk)
439
+ stmt = insert(
440
+ models.Source
441
+ )
442
+ session.execute(stmt, sources_chunk)
341
443
 
342
444
  add_messages(messages, session=session)
343
445
 
@@ -387,13 +489,13 @@ def list_and_mark_transfer_requests_and_source_replicas(
387
489
 
388
490
  if partition_hash_var is None:
389
491
  partition_hash_var = 'requests.id'
390
-
391
492
  if request_state is None:
392
493
  request_state = RequestState.QUEUED
393
-
394
494
  if request_type is None:
395
495
  request_type = [RequestType.TRANSFER]
396
496
 
497
+ now = datetime.datetime.utcnow()
498
+
397
499
  sub_requests = select(
398
500
  models.Request.id,
399
501
  models.Request.request_type,
@@ -415,10 +517,17 @@ def list_and_mark_transfer_requests_and_source_replicas(
415
517
  models.Request.priority,
416
518
  models.Request.transfertool
417
519
  ).with_hint(
418
- models.Request, "INDEX(REQUESTS REQUESTS_TYP_STA_UPD_IDX)", 'oracle'
520
+ models.Request,
521
+ 'INDEX(REQUESTS REQUESTS_TYP_STA_UPD_IDX)',
522
+ 'oracle'
523
+ ).where(
524
+ and_(models.Request.state == request_state,
525
+ models.Request.request_type.in_(request_type))
526
+ ).outerjoin(
527
+ models.ReplicationRule,
528
+ models.Request.rule_id == models.ReplicationRule.id
419
529
  ).where(
420
- models.Request.state == request_state,
421
- models.Request.request_type.in_(request_type)
530
+ coalesce(models.ReplicationRule.expires_at, now) >= now
422
531
  ).join(
423
532
  models.RSE,
424
533
  models.RSE.id == models.Request.dest_rse_id
@@ -435,11 +544,9 @@ def list_and_mark_transfer_requests_and_source_replicas(
435
544
 
436
545
  if processed_by:
437
546
  sub_requests = sub_requests.where(
438
- or_(
439
- models.Request.last_processed_by.is_(null()),
547
+ or_(models.Request.last_processed_by.is_(null()),
440
548
  models.Request.last_processed_by != processed_by,
441
- models.Request.last_processed_at < datetime.datetime.utcnow() - datetime.timedelta(seconds=processed_at_delay)
442
- )
549
+ models.Request.last_processed_at < datetime.datetime.utcnow() - datetime.timedelta(seconds=processed_at_delay))
443
550
  )
444
551
 
445
552
  if not ignore_availability:
@@ -454,14 +561,18 @@ def list_and_mark_transfer_requests_and_source_replicas(
454
561
  # if a transfertool is specified make sure to filter for those requests and apply related index
455
562
  if transfertool:
456
563
  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')
564
+ sub_requests = sub_requests.with_hint(models.Request, 'INDEX(REQUESTS REQUESTS_TYP_STA_TRA_ACT_IDX)', 'oracle')
458
565
  else:
459
- sub_requests = sub_requests.with_hint(models.Request, "INDEX(REQUESTS REQUESTS_TYP_STA_UPD_IDX)", 'oracle')
566
+ sub_requests = sub_requests.with_hint(models.Request, 'INDEX(REQUESTS REQUESTS_TYP_STA_UPD_IDX)', 'oracle')
460
567
 
461
568
  if rses:
462
569
  temp_table_cls = temp_table_mngr(session).create_id_table()
463
570
 
464
- session.execute(insert(temp_table_cls), [{'id': rse_id} for rse_id in rses])
571
+ values = [{'id': rse_id} for rse_id in rses]
572
+ stmt = insert(
573
+ temp_table_cls
574
+ )
575
+ session.execute(stmt, values)
465
576
 
466
577
  sub_requests = sub_requests.join(temp_table_cls, temp_table_cls.id == models.RSE.id)
467
578
 
@@ -506,7 +617,9 @@ def list_and_mark_transfer_requests_and_source_replicas(
506
617
  models.RSEFileAssociation.state == ReplicaState.AVAILABLE,
507
618
  sub_requests.c.dest_rse_id != models.RSEFileAssociation.rse_id)
508
619
  ).with_hint(
509
- models.RSEFileAssociation, "INDEX(REPLICAS REPLICAS_PK)", 'oracle'
620
+ models.RSEFileAssociation,
621
+ 'INDEX(REPLICAS REPLICAS_PK)',
622
+ 'oracle'
510
623
  ).outerjoin(
511
624
  models.RSE,
512
625
  and_(models.RSE.id == models.RSEFileAssociation.rse_id,
@@ -516,13 +629,17 @@ def list_and_mark_transfer_requests_and_source_replicas(
516
629
  and_(sub_requests.c.id == models.Source.request_id,
517
630
  models.RSE.id == models.Source.rse_id)
518
631
  ).with_hint(
519
- models.Source, "INDEX(SOURCES SOURCES_PK)", 'oracle'
632
+ models.Source,
633
+ 'INDEX(SOURCES SOURCES_PK)',
634
+ 'oracle'
520
635
  ).outerjoin(
521
636
  models.Distance,
522
637
  and_(sub_requests.c.dest_rse_id == models.Distance.dest_rse_id,
523
638
  models.RSEFileAssociation.rse_id == models.Distance.src_rse_id)
524
639
  ).with_hint(
525
- models.Distance, "INDEX(DISTANCES DISTANCES_PK)", 'oracle'
640
+ models.Distance,
641
+ 'INDEX(DISTANCES DISTANCES_PK)',
642
+ 'oracle'
526
643
  )
527
644
 
528
645
  for attribute in required_source_rse_attrs or ():
@@ -532,8 +649,8 @@ def list_and_mark_transfer_requests_and_source_replicas(
532
649
  select(
533
650
  1
534
651
  ).where(
535
- rse_attr_alias.rse_id == models.RSE.id,
536
- rse_attr_alias.key == attribute
652
+ and_(rse_attr_alias.rse_id == models.RSE.id,
653
+ rse_attr_alias.key == attribute)
537
654
  )
538
655
  )
539
656
  )
@@ -546,19 +663,19 @@ def list_and_mark_transfer_requests_and_source_replicas(
546
663
  if not request:
547
664
  request = RequestWithSources(id_=request_id, request_type=req_type, rule_id=rule_id, scope=scope, name=name,
548
665
  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],
666
+ previous_attempt_id=previous_attempt_id, dest_rse=rse_collection[dest_rse_id],
550
667
  account=account, retry_count=retry_count, priority=priority, transfertool=transfertool,
551
668
  requested_at=requested_at)
552
669
  requests_by_id[request_id] = request
553
670
  # 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])
671
+ if request.request_type == RequestType.STAGEIN and get_rse_attribute(rse_id=dest_rse_id, key=RseAttr.STAGING_REQUIRED, session=session):
672
+ source = RequestSource(rse=rse_collection[dest_rse_id])
556
673
  request.sources.append(source)
557
674
 
558
675
  if replica_rse_id is not None:
559
676
  replica_rse = rse_collection[replica_rse_id]
560
677
  replica_rse.name = replica_rse_name
561
- source = RequestSource(rse_data=replica_rse, file_path=file_path,
678
+ source = RequestSource(rse=replica_rse, file_path=file_path,
562
679
  ranking=source_ranking, distance=distance, url=source_url)
563
680
  request.sources.append(source)
564
681
  if source_rse_id == replica_rse_id:
@@ -572,19 +689,21 @@ def list_and_mark_transfer_requests_and_source_replicas(
572
689
  models.Request.id.in_(chunk)
573
690
  ).execution_options(
574
691
  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
- )
692
+ ).values({
693
+ models.Request.last_processed_by: processed_by,
694
+ models.Request.last_processed_at: datetime.datetime.now()
695
+ })
581
696
  session.execute(stmt)
582
697
 
583
698
  return requests_by_id
584
699
 
585
700
 
586
701
  @read_session
587
- def fetch_paths(request_id, *, session: "Session"):
702
+ def fetch_paths(
703
+ request_id: str,
704
+ *,
705
+ session: "Session"
706
+ ) -> dict[str, list[str]]:
588
707
  """
589
708
  Find the paths for which the provided request is a constituent hop.
590
709
 
@@ -595,11 +714,9 @@ def fetch_paths(request_id, *, session: "Session"):
595
714
  models.TransferHop,
596
715
  ).join(
597
716
  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
- )
717
+ and_(transfer_hop_alias.initial_request_id == models.TransferHop.initial_request_id,
718
+ or_(transfer_hop_alias.request_id == request_id,
719
+ transfer_hop_alias.initial_request_id == request_id))
603
720
  )
604
721
 
605
722
  parents_by_initial_request = {}
@@ -622,24 +739,24 @@ def fetch_paths(request_id, *, session: "Session"):
622
739
  @transactional_session
623
740
  def get_and_mark_next(
624
741
  rse_collection: "RseCollection",
625
- request_type,
626
- state,
742
+ request_type: Union[list[RequestType], RequestType],
743
+ state: Union[list[RequestState], RequestState],
627
744
  processed_by: Optional[str] = None,
628
745
  processed_at_delay: int = 600,
629
746
  limit: int = 100,
630
747
  older_than: "Optional[datetime.datetime]" = None,
631
- rse_id: "Optional[str]" = None,
632
- activity: "Optional[str]" = None,
748
+ rse_id: Optional[str] = None,
749
+ activity: Optional[str] = None,
633
750
  total_workers: int = 0,
634
751
  worker_number: int = 0,
635
- mode_all=False,
636
- hash_variable='id',
637
- activity_shares=None,
638
- include_dependent=True,
639
- transfertool=None,
752
+ mode_all: bool = False,
753
+ hash_variable: str = 'id',
754
+ activity_shares: Optional[dict[str, Any]] = None,
755
+ include_dependent: bool = True,
756
+ transfertool: Optional[str] = None,
640
757
  *,
641
758
  session: "Session"
642
- ):
759
+ ) -> list[dict[str, Any]]:
643
760
  """
644
761
  Retrieve the next requests matching the request type and state.
645
762
  Workers are balanced via hashing to reduce concurrency on database.
@@ -668,46 +785,48 @@ def get_and_mark_next(
668
785
  METRICS.counter('get_next.requests.{request_type}.{state}').labels(request_type=request_type_metric_label, state=state_metric_label).inc()
669
786
 
670
787
  # lists of one element are not allowed by SQLA, so just duplicate the item
671
- if type(request_type) is not list:
788
+ if not isinstance(request_type, list):
672
789
  request_type = [request_type, request_type]
673
790
  elif len(request_type) == 1:
674
791
  request_type = [request_type[0], request_type[0]]
675
- if type(state) is not list:
792
+ if not isinstance(state, list):
676
793
  state = [state, state]
677
794
  elif len(state) == 1:
678
795
  state = [state[0], state[0]]
679
796
 
680
797
  result = []
681
798
  if not activity_shares:
682
- activity_shares = [None]
799
+ activity_shares = [None] # type: ignore
683
800
 
684
- for share in activity_shares:
801
+ for share in activity_shares: # type: ignore
685
802
 
686
803
  query = select(
687
804
  models.Request.id
688
805
  ).where(
689
- models.Request.state.in_(state),
690
- models.Request.request_type.in_(request_type)
806
+ and_(models.Request.state.in_(state),
807
+ models.Request.request_type.in_(request_type))
691
808
  ).order_by(
692
809
  asc(models.Request.updated_at)
693
810
  )
694
811
  if processed_by:
695
812
  query = query.where(
696
- or_(
697
- models.Request.last_processed_by.is_(null()),
813
+ or_(models.Request.last_processed_by.is_(null()),
698
814
  models.Request.last_processed_by != processed_by,
699
- models.Request.last_processed_at < datetime.datetime.utcnow() - datetime.timedelta(seconds=processed_at_delay)
700
- )
815
+ models.Request.last_processed_at < datetime.datetime.utcnow() - datetime.timedelta(seconds=processed_at_delay))
701
816
  )
702
817
  if transfertool:
703
818
  query = query.with_hint(
704
- models.Request, "INDEX(REQUESTS REQUESTS_TYP_STA_TRA_ACT_IDX)", 'oracle'
819
+ models.Request,
820
+ 'INDEX(REQUESTS REQUESTS_TYP_STA_TRA_ACT_IDX)',
821
+ 'oracle'
705
822
  ).where(
706
823
  models.Request.transfertool == transfertool
707
824
  )
708
825
  else:
709
826
  query = query.with_hint(
710
- models.Request, "INDEX(REQUESTS REQUESTS_TYP_STA_UPD_IDX)", 'oracle'
827
+ models.Request,
828
+ 'INDEX(REQUESTS REQUESTS_TYP_STA_UPD_IDX)',
829
+ 'oracle'
711
830
  )
712
831
 
713
832
  if not include_dependent:
@@ -723,24 +842,24 @@ def get_and_mark_next(
723
842
  )
724
843
 
725
844
  if isinstance(older_than, datetime.datetime):
726
- query = query.filter(models.Request.updated_at < older_than)
845
+ query = query.where(models.Request.updated_at < older_than)
727
846
 
728
847
  if rse_id:
729
- query = query.filter(models.Request.dest_rse_id == rse_id)
848
+ query = query.where(models.Request.dest_rse_id == rse_id)
730
849
 
731
850
  if share:
732
- query = query.filter(models.Request.activity == share)
851
+ query = query.where(models.Request.activity == share)
733
852
  elif activity:
734
- query = query.filter(models.Request.activity == activity)
853
+ query = query.where(models.Request.activity == activity)
735
854
 
736
855
  query = filter_thread_work(session=session, query=query, total_threads=total_workers, thread_id=worker_number, hash_variable=hash_variable)
737
856
 
738
857
  if share:
739
- query = query.limit(activity_shares[share])
858
+ query = query.limit(activity_shares[share]) # type: ignore
740
859
  else:
741
860
  query = query.limit(limit)
742
861
 
743
- if session.bind.dialect.name == 'oracle':
862
+ if session.bind.dialect.name == 'oracle': # type: ignore
744
863
  query = select(
745
864
  models.Request
746
865
  ).where(
@@ -765,8 +884,8 @@ def get_and_mark_next(
765
884
 
766
885
  dst_id = res_dict['dest_rse_id']
767
886
  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
887
+ res_dict['dst_rse'] = rse_collection[dst_id].ensure_loaded(load_name=True, load_attributes=True)
888
+ res_dict['src_rse'] = rse_collection[src_id].ensure_loaded(load_name=True, load_attributes=True) if src_id is not None else None
770
889
 
771
890
  result.append(res_dict)
772
891
  else:
@@ -782,12 +901,10 @@ def get_and_mark_next(
782
901
  models.Request.id.in_(chunk)
783
902
  ).execution_options(
784
903
  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
- )
904
+ ).values({
905
+ models.Request.last_processed_by: processed_by,
906
+ models.Request.last_processed_at: datetime.datetime.now()
907
+ })
791
908
  session.execute(stmt)
792
909
 
793
910
  return result
@@ -809,7 +926,7 @@ def update_request(
809
926
  *,
810
927
  raise_on_missing: bool = False,
811
928
  session: "Session",
812
- ):
929
+ ) -> bool:
813
930
 
814
931
  rowcount = 0
815
932
  try:
@@ -861,7 +978,7 @@ def update_request(
861
978
 
862
979
  @METRICS.count_it
863
980
  @transactional_session
864
- def set_request_state(
981
+ def transition_request_state(
865
982
  request_id: str,
866
983
  state: Optional[RequestState] = None,
867
984
  external_id: Optional[str] = None,
@@ -873,52 +990,57 @@ def set_request_state(
873
990
  err_msg: Optional[str] = None,
874
991
  attributes: Optional[dict[str, str]] = None,
875
992
  *,
993
+ request: Optional[dict[str, Any]] = None,
876
994
  session: "Session",
877
- logger=logging.log
878
- ):
995
+ logger: LoggerFunction = logging.log
996
+ ) -> bool:
879
997
  """
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.
998
+ Update the request if its state changed. Return a boolean showing if the request was actually updated or not.
891
999
  """
892
1000
 
893
1001
  # TODO: Should this be a private method?
894
1002
 
895
- request = get_request(request_id, session=session)
1003
+ if request is None:
1004
+ request = get_request(request_id, session=session)
1005
+
896
1006
  if not request:
897
1007
  # The request was deleted in the meantime. Ignore it.
898
1008
  logger(logging.WARNING, "Request %s not found. Cannot set its state to %s", request_id, state)
899
- return
1009
+ return False
1010
+
1011
+ if request['state'] == state:
1012
+ logger(logging.INFO, "Request %s state is already %s. Will skip the update.", request_id, state)
1013
+ return False
900
1014
 
901
1015
  if state in [RequestState.FAILED, RequestState.DONE, RequestState.LOST] and (request["external_id"] != external_id):
902
1016
  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
- )
1017
+ return False
1018
+
1019
+ update_request(
1020
+ request_id=request_id,
1021
+ state=state,
1022
+ transferred_at=transferred_at,
1023
+ started_at=started_at,
1024
+ staging_started_at=staging_started_at,
1025
+ staging_finished_at=staging_finished_at,
1026
+ source_rse_id=source_rse_id,
1027
+ err_msg=err_msg,
1028
+ attributes=attributes,
1029
+ raise_on_missing=True,
1030
+ session=session,
1031
+ )
1032
+ return True
917
1033
 
918
1034
 
919
1035
  @METRICS.count_it
920
1036
  @transactional_session
921
- def set_requests_state_if_possible(request_ids, new_state, *, session: "Session", logger=logging.log):
1037
+ def transition_requests_state_if_possible(
1038
+ request_ids: Iterable[str],
1039
+ new_state: str,
1040
+ *,
1041
+ session: "Session",
1042
+ logger: LoggerFunction = logging.log
1043
+ ) -> None:
922
1044
  """
923
1045
  Bulk update the state of requests. Skips silently if the request_id does not exist.
924
1046
 
@@ -931,7 +1053,7 @@ def set_requests_state_if_possible(request_ids, new_state, *, session: "Session"
931
1053
  try:
932
1054
  for request_id in request_ids:
933
1055
  try:
934
- set_request_state(request_id, new_state, session=session, logger=logger)
1056
+ transition_request_state(request_id, new_state, session=session, logger=logger)
935
1057
  except UnsupportedOperation:
936
1058
  continue
937
1059
  except IntegrityError as error:
@@ -940,7 +1062,11 @@ def set_requests_state_if_possible(request_ids, new_state, *, session: "Session"
940
1062
 
941
1063
  @METRICS.count_it
942
1064
  @transactional_session
943
- def touch_requests_by_rule(rule_id, *, session: "Session"):
1065
+ def touch_requests_by_rule(
1066
+ rule_id: str,
1067
+ *,
1068
+ session: "Session"
1069
+ ) -> None:
944
1070
  """
945
1071
  Update the update time of requests in a rule. Fails silently if no requests on this rule.
946
1072
 
@@ -952,23 +1078,28 @@ def touch_requests_by_rule(rule_id, *, session: "Session"):
952
1078
  stmt = update(
953
1079
  models.Request
954
1080
  ).prefix_with(
955
- "/*+ INDEX(REQUESTS REQUESTS_RULEID_IDX) */", dialect='oracle'
1081
+ '/*+ INDEX(REQUESTS REQUESTS_RULEID_IDX) */',
1082
+ dialect='oracle'
956
1083
  ).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()
1084
+ and_(models.Request.rule_id == rule_id,
1085
+ models.Request.state.in_([RequestState.FAILED, RequestState.DONE, RequestState.LOST, RequestState.NO_SOURCES, RequestState.ONLY_TAPE_SOURCES]),
1086
+ models.Request.updated_at < datetime.datetime.utcnow())
960
1087
  ).execution_options(
961
1088
  synchronize_session=False
962
- ).values(
963
- updated_at=datetime.datetime.utcnow() + datetime.timedelta(minutes=20)
964
- )
1089
+ ).values({
1090
+ models.Request.updated_at: datetime.datetime.utcnow() + datetime.timedelta(minutes=20)
1091
+ })
965
1092
  session.execute(stmt)
966
1093
  except IntegrityError as error:
967
1094
  raise RucioException(error.args)
968
1095
 
969
1096
 
970
1097
  @read_session
971
- def get_request(request_id, *, session: "Session"):
1098
+ def get_request(
1099
+ request_id: str,
1100
+ *,
1101
+ session: "Session"
1102
+ ) -> Optional[dict[str, Any]]:
972
1103
  """
973
1104
  Retrieve a request by its ID.
974
1105
 
@@ -997,7 +1128,14 @@ def get_request(request_id, *, session: "Session"):
997
1128
 
998
1129
  @METRICS.count_it
999
1130
  @read_session
1000
- def get_request_by_did(scope, name, rse_id, request_type=None, *, session: "Session"):
1131
+ def get_request_by_did(
1132
+ scope: InternalScope,
1133
+ name: str,
1134
+ rse_id: str,
1135
+ request_type: Optional[RequestType] = None,
1136
+ *,
1137
+ session: "Session"
1138
+ ) -> dict[str, Any]:
1001
1139
  """
1002
1140
  Retrieve a request by its DID for a destination RSE.
1003
1141
 
@@ -1013,9 +1151,9 @@ def get_request_by_did(scope, name, rse_id, request_type=None, *, session: "Sess
1013
1151
  stmt = select(
1014
1152
  models.Request
1015
1153
  ).where(
1016
- models.Request.scope == scope,
1017
- models.Request.name == name,
1018
- models.Request.dest_rse_id == rse_id
1154
+ and_(models.Request.scope == scope,
1155
+ models.Request.name == name,
1156
+ models.Request.dest_rse_id == rse_id)
1019
1157
  )
1020
1158
  if request_type:
1021
1159
  stmt = stmt.where(
@@ -1039,7 +1177,14 @@ def get_request_by_did(scope, name, rse_id, request_type=None, *, session: "Sess
1039
1177
 
1040
1178
  @METRICS.count_it
1041
1179
  @read_session
1042
- def get_request_history_by_did(scope, name, rse_id, request_type=None, *, session: "Session"):
1180
+ def get_request_history_by_did(
1181
+ scope: InternalScope,
1182
+ name: str,
1183
+ rse_id: str,
1184
+ request_type: Optional[RequestType] = None,
1185
+ *,
1186
+ session: "Session"
1187
+ ) -> dict[str, Any]:
1043
1188
  """
1044
1189
  Retrieve a historical request by its DID for a destination RSE.
1045
1190
 
@@ -1055,9 +1200,9 @@ def get_request_history_by_did(scope, name, rse_id, request_type=None, *, sessio
1055
1200
  stmt = select(
1056
1201
  models.RequestHistory
1057
1202
  ).where(
1058
- models.RequestHistory.scope == scope,
1059
- models.RequestHistory.name == name,
1060
- models.RequestHistory.dest_rse_id == rse_id
1203
+ and_(models.RequestHistory.scope == scope,
1204
+ models.RequestHistory.name == name,
1205
+ models.RequestHistory.dest_rse_id == rse_id)
1061
1206
  )
1062
1207
  if request_type:
1063
1208
  stmt = stmt.where(
@@ -1078,7 +1223,7 @@ def get_request_history_by_did(scope, name, rse_id, request_type=None, *, sessio
1078
1223
  raise RucioException(error.args)
1079
1224
 
1080
1225
 
1081
- def is_intermediate_hop(request):
1226
+ def is_intermediate_hop(request: RequestDict) -> bool:
1082
1227
  """
1083
1228
  Check if the request is an intermediate hop in a multi-hop transfer.
1084
1229
  """
@@ -1088,9 +1233,14 @@ def is_intermediate_hop(request):
1088
1233
 
1089
1234
 
1090
1235
  @transactional_session
1091
- def handle_failed_intermediate_hop(request, *, session: "Session"):
1236
+ def handle_failed_intermediate_hop(
1237
+ request: RequestDict,
1238
+ *,
1239
+ session: "Session"
1240
+ ) -> int:
1092
1241
  """
1093
1242
  Perform housekeeping behind a failed intermediate hop
1243
+ Returns the number of updated requests
1094
1244
  """
1095
1245
  # mark all hops following this one (in any multihop path) as Failed
1096
1246
  new_state = RequestState.FAILED
@@ -1106,20 +1256,25 @@ def handle_failed_intermediate_hop(request, *, session: "Session"):
1106
1256
  stmt = update(
1107
1257
  models.Request
1108
1258
  ).where(
1109
- models.Request.id.in_(dependent_requests),
1110
- models.Request.state.in_([RequestState.QUEUED, RequestState.SUBMITTED]),
1259
+ and_(models.Request.id.in_(dependent_requests),
1260
+ models.Request.state.in_([RequestState.QUEUED, RequestState.SUBMITTED]))
1111
1261
  ).execution_options(
1112
1262
  synchronize_session=False
1113
- ).values(
1114
- state=new_state,
1115
- err_msg=get_transfer_error(new_state, reason=reason),
1116
- )
1263
+ ).values({
1264
+ models.Request.state: new_state,
1265
+ models.Request.err_msg: get_transfer_error(new_state, reason=reason)
1266
+ })
1117
1267
  session.execute(stmt)
1268
+ return len(dependent_requests)
1118
1269
 
1119
1270
 
1120
1271
  @METRICS.count_it
1121
1272
  @transactional_session
1122
- def archive_request(request_id, *, session: "Session"):
1273
+ def archive_request(
1274
+ request_id: str,
1275
+ *,
1276
+ session: "Session"
1277
+ ) -> None:
1123
1278
  """
1124
1279
  Move a request to the history table.
1125
1280
 
@@ -1166,36 +1321,43 @@ def archive_request(request_id, *, session: "Session"):
1166
1321
  time_diff = req['updated_at'] - req['created_at']
1167
1322
  time_diff_s = time_diff.seconds + time_diff.days * 24 * 3600
1168
1323
  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
- )
1324
+ stmt = delete(
1325
+ models.Source
1326
+ ).where(
1327
+ models.Source.request_id == request_id
1175
1328
  )
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
- )
1329
+ session.execute(stmt)
1330
+
1331
+ stmt = delete(
1332
+ models.TransferHop
1333
+ ).where(
1334
+ or_(models.TransferHop.request_id == request_id,
1335
+ models.TransferHop.next_hop_request_id == request_id,
1336
+ models.TransferHop.initial_request_id == request_id)
1184
1337
  )
1185
- session.execute(
1186
- delete(
1187
- models.Request
1188
- ).where(
1189
- models.Request.id == request_id
1190
- )
1338
+ session.execute(stmt)
1339
+
1340
+ stmt = delete(
1341
+ models.Request
1342
+ ).where(
1343
+ models.Request.id == request_id
1191
1344
  )
1345
+ session.execute(stmt)
1192
1346
  except IntegrityError as error:
1193
1347
  raise RucioException(error.args)
1194
1348
 
1195
1349
 
1196
1350
  @METRICS.count_it
1197
1351
  @transactional_session
1198
- def cancel_request_did(scope, name, dest_rse_id, request_type=RequestType.TRANSFER, *, session: "Session", logger=logging.log):
1352
+ def cancel_request_did(
1353
+ scope: InternalScope,
1354
+ name: str,
1355
+ dest_rse_id: str,
1356
+ request_type: RequestType = RequestType.TRANSFER,
1357
+ *,
1358
+ session: "Session",
1359
+ logger: LoggerFunction = logging.log
1360
+ ) -> dict[str, Any]:
1199
1361
  """
1200
1362
  Cancel a request based on a DID and request type.
1201
1363
 
@@ -1214,14 +1376,14 @@ def cancel_request_did(scope, name, dest_rse_id, request_type=RequestType.TRANSF
1214
1376
  models.Request.external_id,
1215
1377
  models.Request.external_host
1216
1378
  ).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
1379
+ and_(models.Request.scope == scope,
1380
+ models.Request.name == name,
1381
+ models.Request.dest_rse_id == dest_rse_id,
1382
+ models.Request.request_type == request_type)
1221
1383
  )
1222
1384
  reqs = session.execute(stmt).all()
1223
1385
  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)))
1386
+ logger(logging.WARNING, 'Tried to cancel non-existent request for DID %s:%s at RSE %s' % (scope, name, get_rse_name(rse_id=dest_rse_id, session=session)))
1225
1387
  except IntegrityError as error:
1226
1388
  raise RucioException(error.args)
1227
1389
 
@@ -1235,7 +1397,12 @@ def cancel_request_did(scope, name, dest_rse_id, request_type=RequestType.TRANSF
1235
1397
 
1236
1398
 
1237
1399
  @read_session
1238
- def get_sources(request_id, rse_id=None, *, session: "Session"):
1400
+ def get_sources(
1401
+ request_id: str,
1402
+ rse_id: Optional[str] = None,
1403
+ *,
1404
+ session: "Session"
1405
+ ) -> Optional[list[dict[str, Any]]]:
1239
1406
  """
1240
1407
  Retrieve sources by its ID.
1241
1408
 
@@ -1270,7 +1437,11 @@ def get_sources(request_id, rse_id=None, *, session: "Session"):
1270
1437
 
1271
1438
 
1272
1439
  @read_session
1273
- def get_heavy_load_rses(threshold, *, session: "Session"):
1440
+ def get_heavy_load_rses(
1441
+ threshold: int,
1442
+ *,
1443
+ session: "Session"
1444
+ ) -> Optional[list[dict[str, Any]]]:
1274
1445
  """
1275
1446
  Retrieve heavy load rses.
1276
1447
 
@@ -1303,17 +1474,613 @@ def get_heavy_load_rses(threshold, *, session: "Session"):
1303
1474
  raise RucioException(error.args)
1304
1475
 
1305
1476
 
1477
+ class TransferStatsManager:
1478
+
1479
+ @dataclass
1480
+ class _StatsRecord:
1481
+ files_failed: int = 0
1482
+ files_done: int = 0
1483
+ bytes_done: int = 0
1484
+
1485
+ def __init__(self):
1486
+ self.lock = threading.Lock()
1487
+
1488
+ retentions = sorted([
1489
+ # resolution, retention
1490
+ (datetime.timedelta(minutes=5), datetime.timedelta(hours=1)),
1491
+ (datetime.timedelta(hours=1), datetime.timedelta(days=1)),
1492
+ (datetime.timedelta(days=1), datetime.timedelta(days=30)),
1493
+ ])
1494
+
1495
+ self.retentions = retentions
1496
+ self.raw_resolution, raw_retention = self.retentions[0]
1497
+
1498
+ self.current_timestamp = datetime.datetime(year=1970, month=1, day=1)
1499
+ self.current_samples = defaultdict()
1500
+ self._rollover_samples(rollover_time=datetime.datetime.utcnow())
1501
+
1502
+ self.record_stats = True
1503
+ self.save_timer = None
1504
+ self.downsample_timer = None
1505
+ self.downsample_period = math.ceil(raw_retention.total_seconds())
1506
+
1507
+ def __enter__(self) -> "TransferStatsManager":
1508
+ self.record_stats = config_get_bool('transfers', 'stats_enabled', default=self.record_stats)
1509
+ downsample_period = config_get_int('transfers', 'stats_downsample_period', default=self.downsample_period)
1510
+ # Introduce some voluntary jitter to reduce the likely-hood of performing this database
1511
+ # operation multiple times in parallel.
1512
+ self.downsample_period = random.randint(downsample_period * 3 // 4, math.ceil(downsample_period * 5 / 4)) # noqa: S311
1513
+ if self.record_stats:
1514
+ self.save_timer = threading.Timer(self.raw_resolution.total_seconds(), self.periodic_save)
1515
+ self.save_timer.start()
1516
+ self.downsample_timer = threading.Timer(self.downsample_period, self.periodic_downsample_and_cleanup)
1517
+ self.downsample_timer.start()
1518
+ return self
1519
+
1520
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
1521
+ if self.save_timer is not None:
1522
+ self.save_timer.cancel()
1523
+ if self.downsample_timer is not None:
1524
+ self.downsample_timer.cancel()
1525
+ if self.record_stats:
1526
+ self.force_save()
1527
+
1528
+ def observe(
1529
+ self,
1530
+ src_rse_id: str,
1531
+ dst_rse_id: str,
1532
+ activity: str,
1533
+ state: RequestState,
1534
+ file_size: int,
1535
+ *,
1536
+ submitted_at: Optional[datetime.datetime] = None,
1537
+ started_at: Optional[datetime.datetime] = None,
1538
+ transferred_at: Optional[datetime.datetime] = None,
1539
+ session: "Optional[Session]" = None
1540
+ ) -> None:
1541
+ """
1542
+ Increment counters for the given (source_rse, destination_rse, activity) as a result of
1543
+ successful or failed transfer.
1544
+ """
1545
+ if not self.record_stats:
1546
+ return
1547
+ now = datetime.datetime.utcnow()
1548
+ with self.lock:
1549
+ save_timestamp, save_samples = now, {}
1550
+ if now >= self.current_timestamp + self.raw_resolution:
1551
+ save_timestamp, save_samples = self._rollover_samples(now)
1552
+
1553
+ if state in (RequestState.DONE, RequestState.FAILED):
1554
+ record = self.current_samples[dst_rse_id, src_rse_id, activity]
1555
+ if state == RequestState.DONE:
1556
+ record.files_done += 1
1557
+ record.bytes_done += file_size
1558
+
1559
+ if submitted_at is not None and started_at is not None:
1560
+ wait_time = (started_at - submitted_at).total_seconds()
1561
+ METRICS.timer(name='wait_time', buckets=TRANSFER_TIME_BUCKETS).observe(wait_time)
1562
+ if transferred_at is not None:
1563
+ transfer_time = (transferred_at - started_at).total_seconds()
1564
+ METRICS.timer(name='transfer_time', buckets=TRANSFER_TIME_BUCKETS).observe(transfer_time)
1565
+ else:
1566
+ record.files_failed += 1
1567
+ if save_samples:
1568
+ self._save_samples(timestamp=save_timestamp, samples=save_samples, session=session)
1569
+
1570
+ def periodic_save(self) -> None:
1571
+ """
1572
+ Save samples to the database if the end of the current recording interval was reached.
1573
+ Opportunistically perform down-sampling.
1574
+ """
1575
+ self.save_timer = threading.Timer(self.raw_resolution.total_seconds(), self.periodic_save)
1576
+ self.save_timer.start()
1577
+
1578
+ now = datetime.datetime.utcnow()
1579
+ with self.lock:
1580
+ save_timestamp, save_samples = now, {}
1581
+ if now >= self.current_timestamp + self.raw_resolution:
1582
+ save_timestamp, save_samples = self._rollover_samples(now)
1583
+ if save_samples:
1584
+ self._save_samples(timestamp=save_timestamp, samples=save_samples)
1585
+
1586
+ @transactional_session
1587
+ def force_save(self, *, session: "Session") -> None:
1588
+ """
1589
+ Commit to the database everything without ensuring that
1590
+ the end of the currently recorded time interval is reached.
1591
+
1592
+ Only to be used for the final save operation on shutdown.
1593
+ """
1594
+ with self.lock:
1595
+ save_timestamp, save_samples = self._rollover_samples(datetime.datetime.utcnow())
1596
+ if save_samples:
1597
+ self._save_samples(timestamp=save_timestamp, samples=save_samples, session=session)
1598
+
1599
+ def _rollover_samples(self, rollover_time: datetime.datetime) -> "tuple[datetime.datetime, Mapping[tuple[str, str, str], TransferStatsManager._StatsRecord]]":
1600
+ previous_samples = (self.current_timestamp, self.current_samples)
1601
+ self.current_samples = defaultdict(lambda: self._StatsRecord())
1602
+ _, self.current_timestamp = next(self.slice_time(self.raw_resolution, start_time=rollover_time + self.raw_resolution))
1603
+ return previous_samples
1604
+
1605
+ @transactional_session
1606
+ def _save_samples(
1607
+ self,
1608
+ timestamp: "datetime.datetime",
1609
+ samples: "Mapping[tuple[str, str, str], TransferStatsManager._StatsRecord]",
1610
+ *,
1611
+ session: "Session"
1612
+ ) -> None:
1613
+ """
1614
+ Commit the provided samples to the database.
1615
+ """
1616
+ rows_to_insert = []
1617
+ for (dst_rse_id, src_rse_id, activity), record in samples.items():
1618
+ rows_to_insert.append({
1619
+ models.TransferStats.timestamp.name: timestamp,
1620
+ models.TransferStats.resolution.name: self.raw_resolution.total_seconds(),
1621
+ models.TransferStats.src_rse_id.name: src_rse_id,
1622
+ models.TransferStats.dest_rse_id.name: dst_rse_id,
1623
+ models.TransferStats.activity.name: activity,
1624
+ models.TransferStats.files_failed.name: record.files_failed,
1625
+ models.TransferStats.files_done.name: record.files_done,
1626
+ models.TransferStats.bytes_done.name: record.bytes_done,
1627
+ })
1628
+ if rows_to_insert:
1629
+ stmt = insert(
1630
+ models.TransferStats
1631
+ )
1632
+ session.execute(stmt, rows_to_insert)
1633
+
1634
+ def periodic_downsample_and_cleanup(self) -> None:
1635
+ """
1636
+ Periodically create lower resolution samples from higher resolution ones.
1637
+ """
1638
+ self.downsample_timer = threading.Timer(self.downsample_period, self.periodic_downsample_and_cleanup)
1639
+ self.downsample_timer.start()
1640
+
1641
+ while self.downsample_and_cleanup():
1642
+ continue
1643
+
1644
+ @read_session
1645
+ def _db_time_ranges(self, *, session: "Session") -> "dict[datetime.timedelta, tuple[datetime.datetime, datetime.datetime]]":
1646
+
1647
+ stmt = select(
1648
+ models.TransferStats.resolution,
1649
+ func.max(models.TransferStats.timestamp),
1650
+ func.min(models.TransferStats.timestamp),
1651
+ ).group_by(
1652
+ models.TransferStats.resolution,
1653
+ )
1654
+ db_time_ranges = {
1655
+ datetime.timedelta(seconds=res): (newest_t, oldest_t)
1656
+ for res, newest_t, oldest_t in session.execute(stmt)
1657
+ }
1658
+ return db_time_ranges
1659
+
1660
+ @transactional_session
1661
+ def downsample_and_cleanup(self, *, session: "Session") -> bool:
1662
+ """
1663
+ Housekeeping of samples in the database:
1664
+ - create lower-resolution (but higher-retention) samples from higher-resolution ones;
1665
+ - delete the samples which are older than the desired retention time.
1666
+ Return True if it thinks there is still more cleanup.
1667
+
1668
+ This function handles safely to be executed in parallel from multiple daemons at the
1669
+ same time. However, this is achieved at the cost of introducing duplicate samples at lower
1670
+ resolution into the database. The possibility of having duplicates at lower resolutions must be
1671
+ considered during work with those sample. Code must tolerate duplicates and avoid double-counting.
1672
+ """
1673
+
1674
+ # Delay processing to leave time for all raw metrics to be correctly saved to the database
1675
+ now = datetime.datetime.utcnow() - 4 * self.raw_resolution
1676
+
1677
+ db_time_ranges = self._db_time_ranges(session=session)
1678
+
1679
+ more_to_delete = False
1680
+ id_temp_table = temp_table_mngr(session).create_id_table()
1681
+ for i in range(1, len(self.retentions)):
1682
+ src_resolution, desired_src_retention = self.retentions[i - 1]
1683
+ dst_resolution, desired_dst_retention = self.retentions[i]
1684
+
1685
+ # Always keep samples at source resolution aligned to the destination resolution interval.
1686
+ # Keep, at least, the amount of samples needed to cover the first interval at
1687
+ # destination resolution, but keep more samples if explicitly configured to do so.
1688
+ oldest_desired_src_timestamp, _ = next(self.slice_time(dst_resolution, start_time=now - desired_src_retention))
1689
+
1690
+ _, oldest_available_src_timestamp = db_time_ranges.get(src_resolution, (None, None))
1691
+ newest_available_dst_timestamp, oldest_available_dst_timestamp = db_time_ranges.get(dst_resolution, (None, None))
1692
+ # Only generate down-samples at destination resolution for interval in which:
1693
+ # - are within the desired retention window
1694
+ oldest_time_to_handle = now - desired_dst_retention - dst_resolution
1695
+ # - we didn't already generate the corresponding sample at destination resolution
1696
+ if newest_available_dst_timestamp:
1697
+ oldest_time_to_handle = max(oldest_time_to_handle, newest_available_dst_timestamp + datetime.timedelta(seconds=1))
1698
+ # - we have samples at source resolution to do it
1699
+ if oldest_available_src_timestamp:
1700
+ oldest_time_to_handle = max(oldest_time_to_handle, oldest_available_src_timestamp)
1701
+ else:
1702
+ oldest_time_to_handle = now
1703
+
1704
+ # Create samples at lower resolution from samples at higher resolution
1705
+ for recent_t, older_t in self.slice_time(dst_resolution, start_time=now, end_time=oldest_time_to_handle):
1706
+ additional_fields = {
1707
+ models.TransferStats.timestamp.name: older_t,
1708
+ models.TransferStats.resolution.name: dst_resolution.total_seconds(),
1709
+ }
1710
+ src_totals = self._load_totals(resolution=src_resolution, recent_t=recent_t, older_t=older_t, session=session)
1711
+ downsample_stats = [stat | additional_fields for stat in src_totals]
1712
+ if downsample_stats:
1713
+ session.execute(insert(models.TransferStats), downsample_stats)
1714
+ if not oldest_available_dst_timestamp or older_t < oldest_available_dst_timestamp:
1715
+ oldest_available_dst_timestamp = older_t
1716
+ if not newest_available_dst_timestamp or older_t > newest_available_dst_timestamp:
1717
+ newest_available_dst_timestamp = older_t
1718
+
1719
+ if oldest_available_dst_timestamp and newest_available_dst_timestamp:
1720
+ db_time_ranges[dst_resolution] = (newest_available_dst_timestamp, oldest_available_dst_timestamp)
1721
+
1722
+ # Delete from the database the samples which are older than desired
1723
+ more_to_delete |= self._cleanup(
1724
+ id_temp_table=id_temp_table,
1725
+ resolution=src_resolution,
1726
+ timestamp=oldest_desired_src_timestamp,
1727
+ session=session
1728
+ )
1729
+
1730
+ # Cleanup samples at the lowest resolution, which were not handled by the previous loop
1731
+ last_resolution, last_retention = self.retentions[-1]
1732
+ _, oldest_desired_timestamp = next(self.slice_time(last_resolution, start_time=now - last_retention))
1733
+ if db_time_ranges.get(last_resolution, (now, now))[1] < oldest_desired_timestamp:
1734
+ more_to_delete |= self._cleanup(
1735
+ id_temp_table=id_temp_table,
1736
+ resolution=last_resolution,
1737
+ timestamp=oldest_desired_timestamp,
1738
+ session=session
1739
+ )
1740
+
1741
+ # Cleanup all resolutions which exist in the database but are not desired by rucio anymore
1742
+ # (probably due to configuration changes).
1743
+ for resolution_to_cleanup in set(db_time_ranges).difference(r[0] for r in self.retentions):
1744
+ more_to_delete |= self._cleanup(
1745
+ id_temp_table=id_temp_table,
1746
+ resolution=resolution_to_cleanup,
1747
+ timestamp=now,
1748
+ session=session
1749
+ )
1750
+ return more_to_delete
1751
+
1752
+ @stream_session
1753
+ def load_totals(
1754
+ self,
1755
+ older_t: "datetime.datetime",
1756
+ dest_rse_id: Optional[str] = None,
1757
+ src_rse_id: Optional[str] = None,
1758
+ activity: Optional[str] = None,
1759
+ by_activity: bool = True,
1760
+ *,
1761
+ session: "Session"
1762
+ ) -> "Iterator[Mapping[str, str | int]]":
1763
+ """
1764
+ Load totals from now up to older_t in the past by automatically picking the best resolution.
1765
+
1766
+ The results will not necessarily be uniquely grouped by src_rse/dest_rse/activity. The caller
1767
+ is responsible for summing identical src_rse/dest_rse/activity pairs to get the actual result
1768
+ """
1769
+
1770
+ db_time_ranges = self._db_time_ranges(session=session)
1771
+
1772
+ oldest_fetched = older_t
1773
+ for resolution, retention in reversed(self.retentions):
1774
+ newest_available_db_timestamp, oldest_available_db_timestamp = db_time_ranges.get(resolution, (None, None))
1775
+
1776
+ if not (newest_available_db_timestamp and oldest_available_db_timestamp):
1777
+ continue
1778
+
1779
+ if newest_available_db_timestamp < oldest_fetched:
1780
+ continue
1781
+
1782
+ yield from self._load_totals(
1783
+ resolution=resolution,
1784
+ recent_t=newest_available_db_timestamp + datetime.timedelta(seconds=1),
1785
+ older_t=oldest_fetched + datetime.timedelta(seconds=1),
1786
+ dest_rse_id=dest_rse_id,
1787
+ src_rse_id=src_rse_id,
1788
+ activity=activity,
1789
+ by_activity=by_activity,
1790
+ session=session,
1791
+ )
1792
+ oldest_fetched = newest_available_db_timestamp + resolution
1793
+
1794
+ @stream_session
1795
+ def _load_totals(
1796
+ self,
1797
+ resolution: "datetime.timedelta",
1798
+ recent_t: "datetime.datetime",
1799
+ older_t: "datetime.datetime",
1800
+ dest_rse_id: Optional[str] = None,
1801
+ src_rse_id: Optional[str] = None,
1802
+ activity: Optional[str] = None,
1803
+ by_activity: bool = True,
1804
+ *,
1805
+ session: "Session"
1806
+ ) -> "Iterator[Mapping[str, Union[str, int]]]":
1807
+ """
1808
+ Load aggregated totals for the given resolution and time interval.
1809
+
1810
+ Ignore multiple values for the same timestamp at downsample resolutions.
1811
+ They are result of concurrent downsample operations (two different
1812
+ daemons performing downsampling at the same time). Very probably,
1813
+ the values are identical. Eve if not, these values must not be counted twice.
1814
+ This is to gracefully handle multiple parallel downsample operations.
1815
+ """
1816
+ grouping: "list[Any]" = [
1817
+ models.TransferStats.src_rse_id,
1818
+ models.TransferStats.dest_rse_id,
1819
+ ]
1820
+ if by_activity:
1821
+ grouping.append(models.TransferStats.activity)
1822
+
1823
+ if resolution == self.raw_resolution:
1824
+ sub_query = select(
1825
+ models.TransferStats.timestamp,
1826
+ *grouping,
1827
+ models.TransferStats.files_failed,
1828
+ models.TransferStats.files_done,
1829
+ models.TransferStats.bytes_done
1830
+ )
1831
+ else:
1832
+ sub_query = select(
1833
+ models.TransferStats.timestamp,
1834
+ *grouping,
1835
+ func.max(models.TransferStats.files_failed).label(models.TransferStats.files_failed.name),
1836
+ func.max(models.TransferStats.files_done).label(models.TransferStats.files_done.name),
1837
+ func.max(models.TransferStats.bytes_done).label(models.TransferStats.bytes_done.name),
1838
+ ).group_by(
1839
+ models.TransferStats.timestamp,
1840
+ *grouping,
1841
+ )
1842
+
1843
+ sub_query = sub_query.where(
1844
+ models.TransferStats.resolution == resolution.total_seconds(),
1845
+ models.TransferStats.timestamp >= older_t,
1846
+ models.TransferStats.timestamp < recent_t
1847
+ )
1848
+ if dest_rse_id:
1849
+ sub_query = sub_query.where(
1850
+ models.TransferStats.dest_rse_id == dest_rse_id
1851
+ )
1852
+ if src_rse_id:
1853
+ sub_query = sub_query.where(
1854
+ models.TransferStats.src_rse_id == src_rse_id
1855
+ )
1856
+ if activity:
1857
+ sub_query = sub_query.where(
1858
+ models.TransferStats.activity == activity
1859
+ )
1860
+
1861
+ sub_query = sub_query.subquery()
1862
+
1863
+ grouping = [
1864
+ sub_query.c.src_rse_id,
1865
+ sub_query.c.dest_rse_id,
1866
+ ]
1867
+ if by_activity:
1868
+ grouping.append(sub_query.c.activity)
1869
+
1870
+ stmt = select(
1871
+ *grouping,
1872
+ func.sum(sub_query.c.files_failed).label(models.TransferStats.files_failed.name),
1873
+ func.sum(sub_query.c.files_done).label(models.TransferStats.files_done.name),
1874
+ func.sum(sub_query.c.bytes_done).label(models.TransferStats.bytes_done.name),
1875
+ ).group_by(
1876
+ *grouping,
1877
+ )
1878
+
1879
+ for row in session.execute(stmt):
1880
+ yield row._asdict()
1881
+
1882
+ @staticmethod
1883
+ def _cleanup(
1884
+ id_temp_table: Any,
1885
+ resolution: "datetime.timedelta",
1886
+ timestamp: "datetime.datetime",
1887
+ limit: "Optional[int]" = 10000,
1888
+ *,
1889
+ session: "Session"
1890
+ ) -> bool:
1891
+ """
1892
+ Delete, from the database, the stats older than the given time.
1893
+ Skip locked rows, to tolerate parallel executions by multiple daemons.
1894
+ """
1895
+ stmt = select(
1896
+ models.TransferStats.id
1897
+ ).where(
1898
+ and_(models.TransferStats.resolution == resolution.total_seconds(),
1899
+ models.TransferStats.timestamp < timestamp)
1900
+ )
1901
+
1902
+ if limit is not None:
1903
+ stmt = stmt.limit(limit)
1904
+
1905
+ # Oracle does not support chaining order_by(), limit(), and
1906
+ # with_for_update(). Use a nested query to overcome this.
1907
+ if session.bind.dialect.name == 'oracle': # type: ignore
1908
+ stmt = select(
1909
+ models.TransferStats.id
1910
+ ).where(
1911
+ models.TransferStats.id.in_(stmt)
1912
+ ).with_for_update(
1913
+ skip_locked=True
1914
+ )
1915
+ else:
1916
+ stmt = stmt.with_for_update(skip_locked=True)
1917
+
1918
+ del_stmt = delete(
1919
+ id_temp_table
1920
+ )
1921
+ session.execute(del_stmt)
1922
+ insert_stmt = insert(
1923
+ id_temp_table
1924
+ ).from_select(
1925
+ ['id'],
1926
+ stmt
1927
+ )
1928
+ session.execute(insert_stmt)
1929
+
1930
+ stmt = delete(
1931
+ models.TransferStats
1932
+ ).where(
1933
+ exists(select(1).where(models.TransferStats.id == id_temp_table.id))
1934
+ ).execution_options(
1935
+ synchronize_session=False
1936
+ )
1937
+ res = session.execute(stmt)
1938
+ return res.rowcount > 0
1939
+
1940
+ @staticmethod
1941
+ def slice_time(
1942
+ resolution: datetime.timedelta,
1943
+ start_time: "Optional[datetime.datetime]" = None,
1944
+ end_time: "Optional[datetime.datetime]" = None
1945
+ ) -> Iterator[tuple[datetime.datetime, datetime.datetime]]:
1946
+ """
1947
+ Iterates, back in time, over time intervals of length `resolution` which are fully
1948
+ included within the input interval (start_time, end_time).
1949
+ Intervals are aligned on boundaries divisible by resolution.
1950
+
1951
+ For example: for start_time=17:09:59, end_time=16:20:01 and resolution = 10minutes, it will yield
1952
+ (17:00:00, 16:50:00), (16:50:00, 16:40:00), (16:40:00, 16:30:00)
1953
+ """
1954
+
1955
+ if start_time is None:
1956
+ start_time = datetime.datetime.utcnow()
1957
+ newer_t = datetime.datetime.fromtimestamp(int(start_time.timestamp()) // resolution.total_seconds() * resolution.total_seconds())
1958
+ older_t = newer_t - resolution
1959
+ while not end_time or older_t >= end_time:
1960
+ yield newer_t, older_t
1961
+ newer_t = older_t
1962
+ older_t = older_t - resolution
1963
+
1964
+
1306
1965
  @read_session
1307
- def get_request_stats(state, *, session: "Session"):
1966
+ def get_request_metrics(
1967
+ dest_rse_id: Optional[str] = None,
1968
+ src_rse_id: Optional[str] = None,
1969
+ activity: Optional[str] = None,
1970
+ group_by_rse_attribute: Optional[str] = None,
1971
+ *,
1972
+ session: "Session"
1973
+ ) -> dict[str, Any]:
1974
+ metrics = {}
1975
+ now = datetime.datetime.utcnow()
1976
+
1977
+ # Add the current queues
1978
+ db_stats = get_request_stats(
1979
+ state=[
1980
+ RequestState.QUEUED,
1981
+ ],
1982
+ src_rse_id=src_rse_id,
1983
+ dest_rse_id=dest_rse_id,
1984
+ activity=activity,
1985
+ session=session,
1986
+ )
1987
+ for stat in db_stats:
1988
+ if not stat.source_rse_id:
1989
+ continue
1990
+
1991
+ resp_elem = metrics.setdefault((stat.source_rse_id, stat.dest_rse_id), {})
1992
+
1993
+ files_elem = resp_elem.setdefault('files', {})
1994
+ files_elem.setdefault('queued', {})[stat.activity] = stat.counter
1995
+ files_elem['queued-total'] = files_elem.get('queued-total', 0) + stat.counter
1996
+
1997
+ bytes_elem = resp_elem.setdefault('bytes', {})
1998
+ bytes_elem.setdefault('queued', {})[stat.activity] = stat.bytes
1999
+ bytes_elem['queued-total'] = bytes_elem.get('queued-total', 0) + stat.bytes
2000
+
2001
+ # Add the historical data
2002
+ for duration, duration_label in (
2003
+ (datetime.timedelta(hours=1), '1h'),
2004
+ (datetime.timedelta(hours=6), '6h')
2005
+ ):
2006
+ db_stats = TransferStatsManager().load_totals(
2007
+ older_t=now - duration,
2008
+ dest_rse_id=dest_rse_id,
2009
+ src_rse_id=src_rse_id,
2010
+ activity=activity,
2011
+ session=session,
2012
+ )
2013
+
2014
+ for stat in db_stats:
2015
+ resp_elem = metrics.setdefault((stat['src_rse_id'], stat['dest_rse_id']), {})
2016
+
2017
+ files_elem = resp_elem.setdefault('files', {})
2018
+ if stat['files_done']:
2019
+ activity_elem = files_elem.setdefault('done', {}).setdefault(stat['activity'], {})
2020
+ activity_elem[duration_label] = activity_elem.get(duration_label, 0) + stat['files_done']
2021
+ files_elem[f'done-total-{duration_label}'] = files_elem.get(f'done-total-{duration_label}', 0) + stat['files_done']
2022
+ if stat['files_failed']:
2023
+ activity_elem = files_elem.setdefault('failed', {}).setdefault(stat['activity'], {})
2024
+ activity_elem[duration_label] = activity_elem.get(duration_label, 0) + stat['files_failed']
2025
+ files_elem[f'failed-total-{duration_label}'] = files_elem.get(f'failed-total-{duration_label}', 0) + stat['files_failed']
2026
+
2027
+ bytes_elem = resp_elem.setdefault('bytes', {})
2028
+ if stat['bytes_done']:
2029
+ activity_elem = bytes_elem.setdefault('done', {}).setdefault(stat['activity'], {})
2030
+ activity_elem[duration_label] = activity_elem.get(duration_label, 0) + stat['bytes_done']
2031
+ bytes_elem[f'done-total-{duration_label}'] = bytes_elem.get(f'done-total-{duration_label}', 0) + stat['bytes_done']
2032
+
2033
+ # Add distances
2034
+ for distance in get_distances(dest_rse_id=dest_rse_id, src_rse_id=src_rse_id):
2035
+ resp_elem = metrics.setdefault((distance['src_rse_id'], distance['dest_rse_id']), {})
2036
+
2037
+ resp_elem['distance'] = distance['distance']
2038
+
2039
+ # Fill RSE names
2040
+ rses = RseCollection(rse_ids=itertools.chain.from_iterable(metrics))
2041
+ rses.ensure_loaded(load_name=True, include_deleted=True)
2042
+ response = {}
2043
+ for (src_id, dst_id), metric in metrics.items():
2044
+ src_rse = rses[src_id]
2045
+ dst_rse = rses[dst_id]
2046
+ metric['src_rse'] = src_rse.name
2047
+ metric['dst_rse'] = dst_rse.name
2048
+
2049
+ if group_by_rse_attribute:
2050
+ src_rse_group = src_rse.attributes.get(group_by_rse_attribute, 'UNKNOWN')
2051
+ dst_rse_group = dst_rse.attributes.get(group_by_rse_attribute, 'UNKNOWN')
2052
+ if src_rse_group is not None and dst_rse_group is not None:
2053
+ response[f'{src_rse_group}:{dst_rse_group}'] = metric
2054
+ else:
2055
+ response[f'{src_rse.name}:{dst_rse.name}'] = metric
2056
+
2057
+ return response
2058
+
2059
+
2060
+ @read_session
2061
+ def get_request_stats(
2062
+ state: Union[RequestState, list[RequestState]],
2063
+ dest_rse_id: Optional[str] = None,
2064
+ src_rse_id: Optional[str] = None,
2065
+ activity: Optional[str] = None,
2066
+ *,
2067
+ session: "Session"
2068
+ ) -> Sequence[
2069
+ """Row[tuple[
2070
+ Optional[InternalAccount],
2071
+ RequestState,
2072
+ uuid.UUID,
2073
+ Optional[uuid.UUID],
2074
+ Optional[str],
2075
+ int,
2076
+ Optional[int]
2077
+ ]]"""
2078
+ ]:
1308
2079
  """
1309
2080
  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
2081
  """
1315
2082
 
1316
- if type(state) is not list:
2083
+ if not isinstance(state, list):
1317
2084
  state = [state]
1318
2085
 
1319
2086
  try:
@@ -1326,10 +2093,12 @@ def get_request_stats(state, *, session: "Session"):
1326
2093
  func.count(1).label('counter'),
1327
2094
  func.sum(models.Request.bytes).label('bytes')
1328
2095
  ).with_hint(
1329
- models.Request, "INDEX(REQUESTS REQUESTS_TYP_STA_UPD_IDX)", 'oracle'
2096
+ models.Request,
2097
+ 'INDEX(REQUESTS REQUESTS_TYP_STA_UPD_IDX)',
2098
+ 'oracle'
1330
2099
  ).where(
1331
- models.Request.state.in_(state),
1332
- models.Request.request_type.in_([RequestType.TRANSFER, RequestType.STAGEIN, RequestType.STAGEOUT])
2100
+ and_(models.Request.state.in_(state),
2101
+ models.Request.request_type.in_([RequestType.TRANSFER, RequestType.STAGEIN, RequestType.STAGEOUT]))
1333
2102
  ).group_by(
1334
2103
  models.Request.account,
1335
2104
  models.Request.state,
@@ -1337,6 +2106,18 @@ def get_request_stats(state, *, session: "Session"):
1337
2106
  models.Request.source_rse_id,
1338
2107
  models.Request.activity,
1339
2108
  )
2109
+ if src_rse_id:
2110
+ stmt = stmt.where(
2111
+ models.Request.source_rse_id == src_rse_id
2112
+ )
2113
+ if dest_rse_id:
2114
+ stmt = stmt.where(
2115
+ models.Request.dest_rse_id == dest_rse_id
2116
+ )
2117
+ if activity:
2118
+ stmt = stmt.where(
2119
+ models.Request.activity == activity
2120
+ )
1340
2121
 
1341
2122
  return session.execute(stmt).all()
1342
2123
 
@@ -1351,7 +2132,7 @@ def release_waiting_requests_per_deadline(
1351
2132
  deadline: int = 1,
1352
2133
  *,
1353
2134
  session: "Session",
1354
- ):
2135
+ ) -> int:
1355
2136
  """
1356
2137
  Release waiting requests that were waiting too long and exceeded the maximum waiting time to be released.
1357
2138
  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.
@@ -1377,18 +2158,18 @@ def release_waiting_requests_per_deadline(
1377
2158
  old_requests_subquery,
1378
2159
  and_(filtered_requests_subquery.c.dataset_name == old_requests_subquery.c.name,
1379
2160
  filtered_requests_subquery.c.dataset_scope == old_requests_subquery.c.scope)
1380
- ).subquery()
2161
+ )
1381
2162
 
1382
2163
  amount_released_requests = update(
1383
2164
  models.Request
1384
2165
  ).where(
1385
- models.Request.id.in_(old_requests_subquery)
2166
+ models.Request.id.in_(old_requests_subquery) # type: ignore
1386
2167
  ).execution_options(
1387
2168
  synchronize_session=False
1388
- ).values(
1389
- {models.Request.state: RequestState.QUEUED}
1390
- )
1391
- return session.execute(amount_released_requests).rowcount
2169
+ ).values({
2170
+ models.Request.state: RequestState.QUEUED
2171
+ })
2172
+ return session.execute(amount_released_requests).rowcount # type: ignore
1392
2173
 
1393
2174
 
1394
2175
  @transactional_session
@@ -1398,17 +2179,17 @@ def release_waiting_requests_per_free_volume(
1398
2179
  volume: int = 0,
1399
2180
  *,
1400
2181
  session: "Session"
1401
- ):
2182
+ ) -> int:
1402
2183
  """
1403
2184
  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
2185
 
1405
2186
  :param dest_rse_id: The destination RSE id.
1406
2187
  :param source_rse_id: The source RSE id
1407
- :param volume: The maximum volume in bytes that should be transfered.
2188
+ :param volume: The maximum volume in bytes that should be transferred.
1408
2189
  :param session: The database session.
1409
2190
  """
1410
2191
 
1411
- dialect = session.bind.dialect.name
2192
+ dialect = session.bind.dialect.name # type: ignore
1412
2193
  if dialect == 'mysql' or dialect == 'sqlite':
1413
2194
  coalesce_func = func.ifnull
1414
2195
  elif dialect == 'oracle':
@@ -1449,17 +2230,17 @@ def release_waiting_requests_per_free_volume(
1449
2230
  filtered_requests_subquery.c.dataset_scope == cumulated_volume_subquery.c.scope)
1450
2231
  ).where(
1451
2232
  cumulated_volume_subquery.c.cum_volume <= volume - sum_volume_active_subquery.c.sum_bytes
1452
- ).subquery()
2233
+ )
1453
2234
 
1454
2235
  amount_released_requests = update(
1455
2236
  models.Request
1456
2237
  ).where(
1457
- models.Request.id.in_(cumulated_volume_subquery)
2238
+ models.Request.id.in_(cumulated_volume_subquery) # type: ignore
1458
2239
  ).execution_options(
1459
2240
  synchronize_session=False
1460
- ).values(
1461
- {models.Request.state: RequestState.QUEUED},
1462
- )
2241
+ ).values({
2242
+ models.Request.state: RequestState.QUEUED
2243
+ })
1463
2244
  return session.execute(amount_released_requests).rowcount
1464
2245
 
1465
2246
 
@@ -1469,7 +2250,7 @@ def create_base_query_grouped_fifo(
1469
2250
  source_rse_id: Optional[str] = None,
1470
2251
  *,
1471
2252
  session: "Session"
1472
- ):
2253
+ ) -> tuple["Subquery", "Subquery"]:
1473
2254
  """
1474
2255
  Build the sqlalchemy queries to filter relevant requests and to group them in datasets.
1475
2256
  Group requests either by same destination RSE or source RSE.
@@ -1478,7 +2259,7 @@ def create_base_query_grouped_fifo(
1478
2259
  :param source_rse_id: The destination RSE id to filter on
1479
2260
  :param session: The database session.
1480
2261
  """
1481
- dialect = session.bind.dialect.name
2262
+ dialect = session.bind.dialect.name # type: ignore
1482
2263
  if dialect == 'mysql' or dialect == 'sqlite':
1483
2264
  coalesce_func = func.ifnull
1484
2265
  elif dialect == 'oracle':
@@ -1486,7 +2267,7 @@ def create_base_query_grouped_fifo(
1486
2267
  else: # dialect == 'postgresql'
1487
2268
  coalesce_func = func.coalesce
1488
2269
 
1489
- # query DIDs that are attached to a collection and add a column indicating the order of attachment in case of mulitple attachments
2270
+ # query DIDs that are attached to a collection and add a column indicating the order of attachment in case of multiple attachments
1490
2271
  attachment_order_subquery = select(
1491
2272
  models.DataIdentifierAssociation.child_name,
1492
2273
  models.DataIdentifierAssociation.child_scope,
@@ -1555,7 +2336,7 @@ def release_waiting_requests_fifo(
1555
2336
  account: Optional[InternalAccount] = None,
1556
2337
  *,
1557
2338
  session: "Session"
1558
- ):
2339
+ ) -> int:
1559
2340
  """
1560
2341
  Release waiting requests. Transfer requests that were requested first, get released first (FIFO).
1561
2342
 
@@ -1567,7 +2348,7 @@ def release_waiting_requests_fifo(
1567
2348
  :param session: The database session.
1568
2349
  """
1569
2350
 
1570
- dialect = session.bind.dialect.name
2351
+ dialect = session.bind.dialect.name # type: ignore
1571
2352
  rowcount = 0
1572
2353
 
1573
2354
  subquery = select(
@@ -1589,12 +2370,11 @@ def release_waiting_requests_fifo(
1589
2370
  if account is not None:
1590
2371
  subquery = subquery.where(models.Request.account == account)
1591
2372
 
1592
- subquery = subquery.subquery()
1593
-
1594
2373
  if dialect == 'mysql':
1595
2374
  # TODO: check if the logic from this `if` is still needed on modern mysql
1596
2375
 
1597
2376
  # join because IN and LIMIT cannot be used together
2377
+ subquery = subquery.subquery()
1598
2378
  subquery = select(
1599
2379
  models.Request.id
1600
2380
  ).join(
@@ -1602,17 +2382,17 @@ def release_waiting_requests_fifo(
1602
2382
  models.Request.id == subquery.c.id
1603
2383
  ).subquery()
1604
2384
  # wrap select to update and select from the same table
1605
- subquery = select(subquery.c.id).subquery()
2385
+ subquery = select(subquery.c.id)
1606
2386
 
1607
2387
  stmt = update(
1608
2388
  models.Request
1609
2389
  ).where(
1610
- models.Request.id.in_(subquery)
2390
+ models.Request.id.in_(subquery) # type: ignore
1611
2391
  ).execution_options(
1612
2392
  synchronize_session=False
1613
- ).values(
1614
- {'state': RequestState.QUEUED}
1615
- )
2393
+ ).values({
2394
+ models.Request.state: RequestState.QUEUED
2395
+ })
1616
2396
  rowcount = session.execute(stmt).rowcount
1617
2397
  return rowcount
1618
2398
 
@@ -1626,16 +2406,16 @@ def release_waiting_requests_grouped_fifo(
1626
2406
  volume: int = 0,
1627
2407
  *,
1628
2408
  session: "Session"
1629
- ):
2409
+ ) -> int:
1630
2410
  """
1631
2411
  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).
2412
+ Also all requests to DIDs that are attached to the same dataset get released, if one children of the dataset is chosen to be released (Grouped FIFO).
1633
2413
 
1634
2414
  :param dest_rse_id: The destination rse id
1635
2415
  :param source_rse_id: The source RSE id.
1636
2416
  :param count: The count to be released. If None, release all waiting requests.
1637
2417
  :param deadline: Maximal waiting time in hours until a dataset gets released.
1638
- :param volume: The maximum volume in bytes that should be transfered.
2418
+ :param volume: The maximum volume in bytes that should be transferred.
1639
2419
  :param session: The database session.
1640
2420
  """
1641
2421
 
@@ -1667,17 +2447,17 @@ def release_waiting_requests_grouped_fifo(
1667
2447
  ).subquery()
1668
2448
 
1669
2449
  # needed for mysql to update and select from the same table
1670
- cumulated_children_subquery = select(cumulated_children_subquery.c.id).subquery()
2450
+ cumulated_children_subquery = select(cumulated_children_subquery.c.id)
1671
2451
 
1672
2452
  stmt = update(
1673
2453
  models.Request
1674
2454
  ).where(
1675
- models.Request.id.in_(cumulated_children_subquery)
2455
+ models.Request.id.in_(cumulated_children_subquery) # type: ignore
1676
2456
  ).execution_options(
1677
2457
  synchronize_session=False
1678
- ).values(
1679
- {models.Request.state: RequestState.QUEUED}
1680
- )
2458
+ ).values({
2459
+ models.Request.state: RequestState.QUEUED
2460
+ })
1681
2461
  amount_updated_requests += session.execute(stmt).rowcount
1682
2462
 
1683
2463
  # release requests where the whole datasets volume fits in the available volume space
@@ -1695,7 +2475,7 @@ def release_all_waiting_requests(
1695
2475
  account: Optional[InternalAccount] = None,
1696
2476
  *,
1697
2477
  session: "Session"
1698
- ):
2478
+ ) -> int:
1699
2479
  """
1700
2480
  Release all waiting requests per destination RSE.
1701
2481
 
@@ -1712,9 +2492,9 @@ def release_all_waiting_requests(
1712
2492
  models.Request.state == RequestState.WAITING,
1713
2493
  ).execution_options(
1714
2494
  synchronize_session=False
1715
- ).values(
1716
- {'state': RequestState.QUEUED}
1717
- )
2495
+ ).values({
2496
+ models.Request.state: RequestState.QUEUED
2497
+ })
1718
2498
  if source_rse_id is not None:
1719
2499
  query = query.where(
1720
2500
  models.Request.source_rse_id == source_rse_id
@@ -1741,7 +2521,7 @@ def release_all_waiting_requests(
1741
2521
  def list_transfer_limits(
1742
2522
  *,
1743
2523
  session: "Session",
1744
- ):
2524
+ ) -> Iterator[dict[str, Any]]:
1745
2525
  stmt = select(
1746
2526
  models.TransferLimit
1747
2527
  )
@@ -1755,7 +2535,7 @@ def _sync_rse_transfer_limit(
1755
2535
  desired_rse_ids: set[str],
1756
2536
  *,
1757
2537
  session: "Session",
1758
- ):
2538
+ ) -> None:
1759
2539
  """
1760
2540
  Ensure that an RSETransferLimit exists in the database for each of the given rses (and only for these rses)
1761
2541
  """
@@ -1771,20 +2551,21 @@ def _sync_rse_transfer_limit(
1771
2551
  rse_limits_to_delete = existing_rse_ids.difference(desired_rse_ids)
1772
2552
 
1773
2553
  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
- ]
2554
+ values = [
2555
+ {'rse_id': rse_id, 'limit_id': limit_id}
2556
+ for rse_id in rse_limits_to_add
2557
+ ]
2558
+ stmt = insert(
2559
+ models.RSETransferLimit
1780
2560
  )
2561
+ session.execute(stmt, values)
1781
2562
 
1782
2563
  if rse_limits_to_delete:
1783
2564
  stmt = delete(
1784
2565
  models.RSETransferLimit
1785
2566
  ).where(
1786
- models.RSETransferLimit.limit_id == limit_id,
1787
- models.RSETransferLimit.rse_id.in_(rse_limits_to_delete)
2567
+ and_(models.RSETransferLimit.limit_id == limit_id,
2568
+ models.RSETransferLimit.rse_id.in_(rse_limits_to_delete))
1788
2569
  )
1789
2570
  session.execute(stmt)
1790
2571
 
@@ -1794,7 +2575,7 @@ def re_sync_all_transfer_limits(
1794
2575
  delete_empty: bool = False,
1795
2576
  *,
1796
2577
  session: "Session",
1797
- ):
2578
+ ) -> None:
1798
2579
  """
1799
2580
  For each TransferLimit in the database, re-evaluate the rse expression and ensure that the
1800
2581
  correct RSETransferLimits are in the database
@@ -1828,7 +2609,7 @@ def set_transfer_limit(
1828
2609
  waitings: Optional[int] = None,
1829
2610
  *,
1830
2611
  session: "Session",
1831
- ):
2612
+ ) -> uuid.UUID:
1832
2613
  """
1833
2614
  Create or update a transfer limit
1834
2615
 
@@ -1851,9 +2632,9 @@ def set_transfer_limit(
1851
2632
  stmt = select(
1852
2633
  models.TransferLimit
1853
2634
  ).where(
1854
- models.TransferLimit.rse_expression == rse_expression,
1855
- models.TransferLimit.activity == activity,
1856
- models.TransferLimit.direction == direction
2635
+ and_(models.TransferLimit.rse_expression == rse_expression,
2636
+ models.TransferLimit.activity == activity,
2637
+ models.TransferLimit.direction == direction)
1857
2638
  )
1858
2639
  limit = session.execute(stmt).scalar_one_or_none()
1859
2640
 
@@ -1913,7 +2694,7 @@ def set_transfer_limit_stats(
1913
2694
  transfers: int,
1914
2695
  *,
1915
2696
  session: "Session",
1916
- ):
2697
+ ) -> None:
1917
2698
  """
1918
2699
  Set the statistics of the TransferLimit
1919
2700
  """
@@ -1921,10 +2702,10 @@ def set_transfer_limit_stats(
1921
2702
  models.TransferLimit
1922
2703
  ).where(
1923
2704
  models.TransferLimit.id == limit_id
1924
- ).values(
1925
- waitings=waitings,
1926
- transfers=transfers
1927
- )
2705
+ ).values({
2706
+ models.TransferLimit.waitings: waitings,
2707
+ models.TransferLimit.transfers: transfers
2708
+ })
1928
2709
  session.execute(stmt)
1929
2710
 
1930
2711
 
@@ -1935,7 +2716,7 @@ def delete_transfer_limit(
1935
2716
  direction: TransferLimitDirection = TransferLimitDirection.DESTINATION,
1936
2717
  *,
1937
2718
  session: "Session",
1938
- ):
2719
+ ) -> None:
1939
2720
 
1940
2721
  if activity is None:
1941
2722
  activity = 'all_activities'
@@ -1946,10 +2727,10 @@ def delete_transfer_limit(
1946
2727
  exists(
1947
2728
  select(1)
1948
2729
  ).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
2730
+ and_(models.RSETransferLimit.limit_id == models.TransferLimit.id,
2731
+ models.TransferLimit.rse_expression == rse_expression,
2732
+ models.TransferLimit.activity == activity,
2733
+ models.TransferLimit.direction == direction)
1953
2734
  )
1954
2735
  ).execution_options(
1955
2736
  synchronize_session=False
@@ -1959,9 +2740,9 @@ def delete_transfer_limit(
1959
2740
  stmt = delete(
1960
2741
  models.TransferLimit
1961
2742
  ).where(
1962
- models.TransferLimit.rse_expression == rse_expression,
1963
- models.TransferLimit.activity == activity,
1964
- models.TransferLimit.direction == direction
2743
+ and_(models.TransferLimit.rse_expression == rse_expression,
2744
+ models.TransferLimit.activity == activity,
2745
+ models.TransferLimit.direction == direction)
1965
2746
  )
1966
2747
  session.execute(stmt)
1967
2748
 
@@ -1971,7 +2752,7 @@ def delete_transfer_limit_by_id(
1971
2752
  limit_id: str,
1972
2753
  *,
1973
2754
  session: "Session",
1974
- ):
2755
+ ) -> None:
1975
2756
  stmt = delete(
1976
2757
  models.RSETransferLimit
1977
2758
  ).where(
@@ -1988,7 +2769,13 @@ def delete_transfer_limit_by_id(
1988
2769
 
1989
2770
 
1990
2771
  @transactional_session
1991
- def update_requests_priority(priority, filter_, *, session: "Session", logger=logging.log):
2772
+ def update_requests_priority(
2773
+ priority: int,
2774
+ filter_: FilterDict,
2775
+ *,
2776
+ session: "Session",
2777
+ logger: LoggerFunction = logging.log
2778
+ ) -> dict[str, Any]:
1992
2779
  """
1993
2780
  Update priority of requests.
1994
2781
 
@@ -2011,13 +2798,13 @@ def update_requests_priority(priority, filter_, *, session: "Session", logger=lo
2011
2798
  models.ReplicaLock.rse_id == models.Request.dest_rse_id)
2012
2799
  )
2013
2800
  if 'rule_id' in filter_:
2014
- query = query.filter(models.ReplicaLock.rule_id == filter_['rule_id'])
2801
+ query = query.where(models.ReplicaLock.rule_id == filter_['rule_id'])
2015
2802
  if 'request_id' in filter_:
2016
- query = query.filter(models.Request.id == filter_['request_id'])
2803
+ query = query.where(models.Request.id == filter_['request_id'])
2017
2804
  if 'older_than' in filter_:
2018
- query = query.filter(models.Request.created_at < filter_['older_than'])
2805
+ query = query.where(models.Request.created_at < filter_['older_than'])
2019
2806
  if 'activities' in filter_:
2020
- if type(filter_['activities']) is not list:
2807
+ if not isinstance(filter_['activities'], list):
2021
2808
  filter_['activities'] = filter_['activities'].split(',')
2022
2809
  query = query.filter(models.Request.activity.in_(filter_['activities']))
2023
2810
 
@@ -2036,7 +2823,13 @@ def update_requests_priority(priority, filter_, *, session: "Session", logger=lo
2036
2823
 
2037
2824
 
2038
2825
  @read_session
2039
- def add_monitor_message(new_state, request, additional_fields, *, session: "Session"):
2826
+ def add_monitor_message(
2827
+ new_state: RequestState,
2828
+ request: RequestDict,
2829
+ additional_fields: "Mapping[str, Any]",
2830
+ *,
2831
+ session: "Session"
2832
+ ) -> None:
2040
2833
  """
2041
2834
  Create a message for hermes from a request
2042
2835
 
@@ -2055,8 +2848,8 @@ def add_monitor_message(new_state, request, additional_fields, *, session: "Sess
2055
2848
  stmt = select(
2056
2849
  models.DataIdentifier.datatype
2057
2850
  ).where(
2058
- models.DataIdentifier.scope == request['scope'],
2059
- models.DataIdentifier.name == request['name'],
2851
+ and_(models.DataIdentifier.scope == request['scope'],
2852
+ models.DataIdentifier.name == request['name'])
2060
2853
  )
2061
2854
  datatype = session.execute(stmt).scalar_one_or_none()
2062
2855
 
@@ -2132,7 +2925,10 @@ def add_monitor_message(new_state, request, additional_fields, *, session: "Sess
2132
2925
  add_message(transfer_status, message, session=session)
2133
2926
 
2134
2927
 
2135
- def get_transfer_error(state, reason=None):
2928
+ def get_transfer_error(
2929
+ state: RequestState,
2930
+ reason: Optional[str] = None
2931
+ ) -> Optional[str]:
2136
2932
  """
2137
2933
  Transform a specific RequestState to an error message
2138
2934
 
@@ -2157,7 +2953,13 @@ def get_transfer_error(state, reason=None):
2157
2953
 
2158
2954
 
2159
2955
  @read_session
2160
- def get_source_rse(request_id, src_url, *, session: "Session", logger=logging.log):
2956
+ def get_source_rse(
2957
+ request_id: str,
2958
+ src_url: str,
2959
+ *,
2960
+ session: "Session",
2961
+ logger: LoggerFunction = logging.log
2962
+ ) -> tuple[Optional[str], Optional[str]]:
2161
2963
  """
2162
2964
  Based on a request, and src_url extract the source rse name and id.
2163
2965
 
@@ -2179,7 +2981,7 @@ def get_source_rse(request_id, src_url, *, session: "Session", logger=logging.lo
2179
2981
  src_rse_name = get_rse_name(src_rse_id, session=session)
2180
2982
  logger(logging.DEBUG, "Find rse name %s for %s" % (src_rse_name, src_url))
2181
2983
  return src_rse_name, src_rse_id
2182
- # cannot find matched surl
2984
+ # cannot find matched source url
2183
2985
  logger(logging.WARNING, 'Cannot get correct RSE for source url: %s' % (src_url))
2184
2986
  return None, None
2185
2987
  except Exception:
@@ -2188,7 +2990,13 @@ def get_source_rse(request_id, src_url, *, session: "Session", logger=logging.lo
2188
2990
 
2189
2991
 
2190
2992
  @stream_session
2191
- def list_requests(src_rse_ids, dst_rse_ids, states=None, *, session: "Session"):
2993
+ def list_requests(
2994
+ src_rse_ids: Sequence[str],
2995
+ dst_rse_ids: Sequence[str],
2996
+ states: Optional[Sequence[RequestState]] = None,
2997
+ *,
2998
+ session: "Session"
2999
+ ) -> Iterator[models.Request]:
2192
3000
  """
2193
3001
  List all requests in a specific state from a source RSE to a destination RSE.
2194
3002
 
@@ -2203,16 +3011,24 @@ def list_requests(src_rse_ids, dst_rse_ids, states=None, *, session: "Session"):
2203
3011
  stmt = select(
2204
3012
  models.Request
2205
3013
  ).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)
3014
+ and_(models.Request.state.in_(states),
3015
+ models.Request.source_rse_id.in_(src_rse_ids),
3016
+ models.Request.dest_rse_id.in_(dst_rse_ids))
2209
3017
  )
2210
3018
  for request in session.execute(stmt).yield_per(500).scalars():
2211
3019
  yield request
2212
3020
 
2213
3021
 
2214
3022
  @stream_session
2215
- def list_requests_history(src_rse_ids, dst_rse_ids, states=None, offset=None, limit=None, *, session: "Session"):
3023
+ def list_requests_history(
3024
+ src_rse_ids: Sequence[str],
3025
+ dst_rse_ids: Sequence[str],
3026
+ states: Optional[Sequence[RequestState]] = None,
3027
+ offset: Optional[int] = None,
3028
+ limit: Optional[int] = None,
3029
+ *,
3030
+ session: "Session"
3031
+ ) -> Iterator[models.RequestHistory]:
2216
3032
  """
2217
3033
  List all historical requests in a specific state from a source RSE to a destination RSE.
2218
3034
 
@@ -2228,10 +3044,10 @@ def list_requests_history(src_rse_ids, dst_rse_ids, states=None, offset=None, li
2228
3044
 
2229
3045
  stmt = select(
2230
3046
  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)
3047
+ ).where(
3048
+ and_(models.RequestHistory.state.in_(states),
3049
+ models.RequestHistory.source_rse_id.in_(src_rse_ids),
3050
+ models.RequestHistory.dest_rse_id.in_(dst_rse_ids))
2235
3051
  )
2236
3052
  if offset:
2237
3053
  stmt = stmt.offset(offset)
@@ -2239,3 +3055,35 @@ def list_requests_history(src_rse_ids, dst_rse_ids, states=None, offset=None, li
2239
3055
  stmt = stmt.limit(limit)
2240
3056
  for request in session.execute(stmt).yield_per(500).scalars():
2241
3057
  yield request
3058
+
3059
+
3060
+ @transactional_session
3061
+ def reset_stale_waiting_requests(time_limit: Optional[datetime.timedelta] = datetime.timedelta(days=1), *, session: "Session") -> None:
3062
+ """
3063
+ Clear source_rse_id for requests that have been in the waiting state for > time_limit amount of time and
3064
+ transition back to preparing state (default time limit = 1 day).
3065
+ This allows for stale requests that have been in the waiting state for a long time to be able to
3066
+ react to source changes that have occurred in the meantime.
3067
+ :param time_limit: The amount of time a request must be in the waiting state to be reset.
3068
+ :param session: The database session in use.
3069
+ """
3070
+ try:
3071
+ # Cutoff timestamp based on time limit
3072
+ time_limit_timestamp = datetime.datetime.utcnow() - time_limit
3073
+
3074
+ # Select all waiting requests that precede the time limit, then clear source_rse_id and reset state to preparing
3075
+ stmt = update(
3076
+ models.Request
3077
+ ).where(
3078
+ and_(models.Request.state == RequestState.WAITING,
3079
+ models.Request.last_processed_at < time_limit_timestamp)
3080
+ ).execution_options(
3081
+ synchronize_session=False
3082
+ ).values({
3083
+ models.Request.source_rse_id: None,
3084
+ models.Request.state: RequestState.PREPARING
3085
+ })
3086
+ session.execute(stmt)
3087
+
3088
+ except IntegrityError as error:
3089
+ raise RucioException(error.args)