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/transfer.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");
@@ -15,10 +14,13 @@
15
14
 
16
15
  import datetime
17
16
  import logging
17
+ import operator
18
18
  import re
19
+ import sys
19
20
  import time
20
21
  import traceback
21
- from typing import TYPE_CHECKING
22
+ from collections import defaultdict
23
+ from typing import TYPE_CHECKING, cast
22
24
 
23
25
  from dogpile.cache import make_region
24
26
  from dogpile.cache.api import NoValue
@@ -26,33 +28,37 @@ from sqlalchemy import select, update
26
28
  from sqlalchemy.exc import IntegrityError
27
29
 
28
30
  from rucio.common import constants
29
- from rucio.common.config import config_get
30
- from rucio.common.constants import SUPPORTED_PROTOCOLS
31
- from rucio.common.exception import (InvalidRSEExpression,
32
- RequestNotFound, RSEProtocolNotSupported,
33
- RucioException, UnsupportedOperation)
34
- from rucio.common.utils import construct_surl
35
- from rucio.core import did, message as message_core, request as request_core
31
+ from rucio.common.config import config_get, config_get_list
32
+ from rucio.common.constants import SUPPORTED_PROTOCOLS, RseAttr
33
+ from rucio.common.exception import InvalidRSEExpression, RequestNotFound, RSEProtocolNotSupported, RucioException, UnsupportedOperation
34
+ from rucio.common.utils import construct_non_deterministic_pfn
35
+ from rucio.core import did
36
+ from rucio.core import message as message_core
37
+ from rucio.core import request as request_core
36
38
  from rucio.core.account import list_accounts
37
39
  from rucio.core.monitor import MetricManager
38
- from rucio.core.request import set_request_state, RequestWithSources, RequestSource
40
+ from rucio.core.request import DirectTransfer, RequestSource, RequestWithSources, TransferDestination, transition_request_state
39
41
  from rucio.core.rse import RseData
40
42
  from rucio.core.rse_expression_parser import parse_expression
41
43
  from rucio.db.sqla import models
42
44
  from rucio.db.sqla.constants import DIDType, RequestState, RequestType, TransferLimitDirection
43
- from rucio.db.sqla.session import read_session, transactional_session, stream_session
45
+ from rucio.db.sqla.session import read_session, stream_session, transactional_session
44
46
  from rucio.rse import rsemanager as rsemgr
45
- from rucio.transfertool.transfertool import TransferStatusReport
47
+ from rucio.transfertool.bittorrent import BittorrentTransfertool
46
48
  from rucio.transfertool.fts3 import FTS3Transfertool
47
49
  from rucio.transfertool.globus import GlobusTransferTool
48
50
  from rucio.transfertool.mock import MockTransfertool
51
+ from rucio.transfertool.transfertool import TransferStatusReport, Transfertool
49
52
 
50
53
  if TYPE_CHECKING:
51
- from collections.abc import Callable, Generator, Iterable
54
+ from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
52
55
  from typing import Any, Optional
56
+
53
57
  from sqlalchemy.orm import Session
58
+
54
59
  from rucio.common.types import InternalAccount
55
60
  from rucio.core.topology import Topology
61
+ from rucio.rse.protocols.protocol import RSEProtocol
56
62
 
57
63
  LoggerFunction = Callable[..., Any]
58
64
 
@@ -69,22 +75,14 @@ WEBDAV_TRANSFER_MODE = config_get('conveyor', 'webdav_transfer_mode', False, Non
69
75
 
70
76
  DEFAULT_MULTIHOP_TOMBSTONE_DELAY = int(datetime.timedelta(hours=2).total_seconds())
71
77
 
72
- TRANSFERTOOL_CLASSES_BY_NAME = {
78
+ TRANSFERTOOL_CLASSES_BY_NAME: "dict[str, type[Transfertool]]" = {
73
79
  FTS3Transfertool.external_name: FTS3Transfertool,
74
80
  GlobusTransferTool.external_name: GlobusTransferTool,
75
81
  MockTransfertool.external_name: MockTransfertool,
82
+ BittorrentTransfertool.external_name: BittorrentTransfertool,
76
83
  }
77
84
 
78
85
 
79
- class TransferDestination:
80
- def __init__(self, rse_data, scheme):
81
- self.rse = rse_data
82
- self.scheme = scheme
83
-
84
- def __str__(self):
85
- return "dst_rse={}".format(self.rse)
86
-
87
-
88
86
  class ProtocolFactory:
89
87
  """
90
88
  Creates and caches protocol objects. Allowing to reuse them.
@@ -92,16 +90,16 @@ class ProtocolFactory:
92
90
  def __init__(self):
93
91
  self.protocols = {}
94
92
 
95
- def protocol(self, rse_data, scheme, operation):
96
- protocol_key = '%s_%s_%s' % (operation, rse_data.id, scheme)
93
+ def protocol(self, rse: RseData, scheme: "Optional[str]", operation: str):
94
+ protocol_key = '%s_%s_%s' % (operation, rse.id, scheme)
97
95
  protocol = self.protocols.get(protocol_key)
98
96
  if not protocol:
99
- protocol = rsemgr.create_protocol(rse_data.info, operation, scheme)
97
+ protocol = rsemgr.create_protocol(rse.info, operation, scheme)
100
98
  self.protocols[protocol_key] = protocol
101
99
  return protocol
102
100
 
103
101
 
104
- class DirectTransferDefinition:
102
+ class DirectTransferImplementation(DirectTransfer):
105
103
  """
106
104
  The configuration for a direct (non-multi-hop) transfer. It can be a multi-source transfer.
107
105
 
@@ -110,16 +108,15 @@ class DirectTransferDefinition:
110
108
  """
111
109
  def __init__(self, source: RequestSource, destination: TransferDestination, rws: RequestWithSources,
112
110
  protocol_factory: ProtocolFactory, operation_src: str, operation_dest: str):
113
- self.sources = [source]
111
+ super().__init__(sources=[source], rws=rws)
114
112
  self.destination = destination
115
113
 
116
- self.rws = rws
117
114
  self.protocol_factory = protocol_factory
118
115
  self.operation_src = operation_src
119
116
  self.operation_dest = operation_dest
120
117
 
121
118
  self._dest_url = None
122
- self._legacy_sources = None
119
+ self._source_urls = {}
123
120
 
124
121
  def __str__(self):
125
122
  return '{sources}--{request_id}->{destination}'.format(
@@ -129,40 +126,36 @@ class DirectTransferDefinition:
129
126
  )
130
127
 
131
128
  @property
132
- def src(self):
129
+ def src(self) -> RequestSource:
133
130
  return self.sources[0]
134
131
 
135
132
  @property
136
- def dst(self):
133
+ def dst(self) -> TransferDestination:
137
134
  return self.destination
138
135
 
139
136
  @property
140
- def dest_url(self):
137
+ def dest_url(self) -> str:
141
138
  if not self._dest_url:
142
139
  self._dest_url = self._generate_dest_url(self.dst, self.rws, self.protocol_factory, self.operation_dest)
143
140
  return self._dest_url
144
141
 
145
- @property
146
- def legacy_sources(self):
147
- if not self._legacy_sources:
148
- self._legacy_sources = [
149
- (src.rse.name,
150
- self._generate_source_url(src,
151
- self.dst,
152
- rws=self.rws,
153
- protocol_factory=self.protocol_factory,
154
- operation=self.operation_src),
155
- src.rse.id,
156
- src.ranking)
157
- for src in self.sources
158
- ]
159
- return self._legacy_sources
142
+ def source_url(self, source: RequestSource) -> str:
143
+ url = self._source_urls.get(source.rse)
144
+ if not url:
145
+ self._source_urls[source.rse] = url = self._generate_source_url(
146
+ source,
147
+ self.dst,
148
+ rws=self.rws,
149
+ protocol_factory=self.protocol_factory,
150
+ operation=self.operation_src
151
+ )
152
+ return url
160
153
 
161
- @property
162
- def use_ipv4(self):
163
- # If any source or destination rse is ipv4 only
164
- return self.dst.rse.attributes.get('use_ipv4', False) or any(src.rse.attributes.get('use_ipv4', False)
165
- for src in self.sources)
154
+ def dest_protocol(self) -> "RSEProtocol":
155
+ return self.protocol_factory.protocol(self.dst.rse, self.dst.scheme, self.operation_dest)
156
+
157
+ def source_protocol(self, source: RequestSource) -> "RSEProtocol":
158
+ return self.protocol_factory.protocol(source.rse, source.scheme, self.operation_src)
166
159
 
167
160
  @staticmethod
168
161
  def __rewrite_source_url(source_url, source_sign_url, dest_sign_url, source_scheme):
@@ -215,8 +208,8 @@ class DirectTransferDefinition:
215
208
  protocol = protocol_factory.protocol(src.rse, src.scheme, operation)
216
209
 
217
210
  # Compute the source URL
218
- source_sign_url = src.rse.attributes.get('sign_url', None)
219
- dest_sign_url = dst.rse.attributes.get('sign_url', None)
211
+ source_sign_url = src.rse.attributes.get(RseAttr.SIGN_URL, None)
212
+ dest_sign_url = dst.rse.attributes.get(RseAttr.SIGN_URL, None)
220
213
  source_url = list(protocol.lfns2pfns(lfns={'scope': rws.scope.external, 'name': rws.name, 'path': src.file_path}).values())[0]
221
214
  source_url = cls.__rewrite_source_url(source_url, source_sign_url=source_sign_url, dest_sign_url=dest_sign_url, source_scheme=src.scheme)
222
215
  return source_url
@@ -236,35 +229,44 @@ class DirectTransferDefinition:
236
229
  # naming convention, etc.
237
230
  dsn = get_dsn(rws.scope, rws.name, rws.attributes.get('dsn', None))
238
231
  # DQ2 path always starts with /, but prefix might not end with /
239
- naming_convention = dst.rse.attributes.get('naming_convention', None)
240
- dest_path = construct_surl(dsn, rws.scope.external, rws.name, naming_convention)
232
+ naming_convention = dst.rse.attributes.get(RseAttr.NAMING_CONVENTION, None)
233
+ if rws.scope.external is not None:
234
+ dest_path = construct_non_deterministic_pfn(dsn, rws.scope.external, rws.name, naming_convention)
241
235
  if dst.rse.is_tape():
242
236
  if rws.retry_count or rws.activity == 'Recovery':
243
237
  dest_path = '%s_%i' % (dest_path, int(time.time()))
244
238
 
245
239
  dest_url = list(protocol.lfns2pfns(lfns={'scope': rws.scope.external, 'name': rws.name, 'path': dest_path}).values())[0]
246
240
 
247
- dest_sign_url = dst.rse.attributes.get('sign_url', None)
241
+ dest_sign_url = dst.rse.attributes.get(RseAttr.SIGN_URL, None)
248
242
  dest_url = cls.__rewrite_dest_url(dest_url, dest_sign_url=dest_sign_url)
249
243
  return dest_url
250
244
 
251
245
 
252
- class StageinTransferDefinition(DirectTransferDefinition):
246
+ class StageinTransferImplementation(DirectTransferImplementation):
253
247
  """
254
248
  A definition of a transfer which triggers a stagein operation.
255
249
  - The source and destination url are identical
256
250
  - must be from TAPE to non-TAPE RSE
257
251
  - can only have one source
258
252
  """
259
- def __init__(self, source, destination, rws, protocol_factory, operation_src, operation_dest):
253
+ def __init__(
254
+ self,
255
+ source: RequestSource,
256
+ destination: TransferDestination,
257
+ rws: RequestWithSources,
258
+ protocol_factory: ProtocolFactory,
259
+ operation_src: str,
260
+ operation_dest: str
261
+ ):
260
262
  if not source.rse.is_tape() or destination.rse.is_tape():
261
263
  # allow staging_required QoS RSE to be TAPE to TAPE for pin
262
- if not destination.rse.attributes.get('staging_required', None):
264
+ if not destination.rse.attributes.get(RseAttr.STAGING_REQUIRED, None):
263
265
  raise RucioException("Stageing request {} must be from TAPE to DISK rse. Got {} and {}.".format(rws, source, destination))
264
266
  super().__init__(source, destination, rws, protocol_factory, operation_src, operation_dest)
265
267
 
266
268
  @property
267
- def dest_url(self):
269
+ def dest_url(self) -> str:
268
270
  if not self._dest_url:
269
271
  self._dest_url = self.src.url if self.src.url else self._generate_source_url(self.src,
270
272
  self.dst,
@@ -273,19 +275,12 @@ class StageinTransferDefinition(DirectTransferDefinition):
273
275
  operation=self.operation_dest)
274
276
  return self._dest_url
275
277
 
276
- @property
277
- def legacy_sources(self):
278
- if not self._legacy_sources:
279
- self._legacy_sources = [(
280
- self.src.rse.name,
281
- self.dest_url, # Source and dest url is the same for stagein requests
282
- self.src.rse.id,
283
- self.src.ranking
284
- )]
285
- return self._legacy_sources
286
-
287
-
288
- def transfer_path_str(transfer_path: "list[DirectTransferDefinition]") -> str:
278
+ def source_url(self, source: RequestSource) -> str:
279
+ # Source and dest url is the same for stagein requests
280
+ return self.dest_url
281
+
282
+
283
+ def transfer_path_str(transfer_path: "list[DirectTransfer]") -> str:
289
284
  """
290
285
  an implementation of __str__ for a transfer path, which is a list of direct transfers, so not really an object
291
286
  """
@@ -312,7 +307,7 @@ def transfer_path_str(transfer_path: "list[DirectTransferDefinition]") -> str:
312
307
 
313
308
  @transactional_session
314
309
  def mark_submitting(
315
- transfer: "DirectTransferDefinition",
310
+ transfer: "DirectTransfer",
316
311
  external_host: str,
317
312
  *,
318
313
  logger: "Callable",
@@ -329,7 +324,7 @@ def mark_submitting(
329
324
  transfer.rws.scope,
330
325
  transfer.rws.name,
331
326
  transfer.rws.previous_attempt_id,
332
- transfer.legacy_sources,
327
+ [transfer.source_url(s) for s in transfer.sources],
333
328
  transfer.dest_url,
334
329
  external_host)
335
330
  logger(logging.DEBUG, "%s", log_str)
@@ -358,7 +353,7 @@ def mark_submitting(
358
353
 
359
354
  @transactional_session
360
355
  def ensure_db_sources(
361
- transfer_path: "list[DirectTransferDefinition]",
356
+ transfer_path: "list[DirectTransfer]",
362
357
  *,
363
358
  logger: "Callable",
364
359
  session: "Session",
@@ -370,15 +365,15 @@ def ensure_db_sources(
370
365
  desired_sources = []
371
366
  for transfer in transfer_path:
372
367
 
373
- for src_rse, src_url, src_rse_id, rank in transfer.legacy_sources:
368
+ for source in transfer.sources:
374
369
  common_source_attrs = {
375
370
  "scope": transfer.rws.scope,
376
371
  "name": transfer.rws.name,
377
- "rse_id": src_rse_id,
372
+ "rse_id": source.rse.id,
378
373
  "dest_rse_id": transfer.dst.rse.id,
379
- "ranking": rank if rank else 0,
374
+ "ranking": source.ranking,
380
375
  "bytes": transfer.rws.byte_count,
381
- "url": src_url,
376
+ "url": transfer.source_url(source),
382
377
  "is_using": True,
383
378
  }
384
379
 
@@ -502,7 +497,13 @@ def set_transfers_state(
502
497
 
503
498
 
504
499
  @transactional_session
505
- def update_transfer_state(tt_status_report: TransferStatusReport, *, session: "Session", logger=logging.log):
500
+ def update_transfer_state(
501
+ tt_status_report: TransferStatusReport,
502
+ stats_manager: request_core.TransferStatsManager,
503
+ *,
504
+ session: "Session",
505
+ logger=logging.log
506
+ ):
506
507
  """
507
508
  Used by poller and consumer to update the internal state of requests,
508
509
  after the response by the external transfertool.
@@ -510,10 +511,11 @@ def update_transfer_state(tt_status_report: TransferStatusReport, *, session: "S
510
511
  :param tt_status_report: The transfertool status update, retrieved via request.query_request().
511
512
  :param session: The database session to use.
512
513
  :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
513
- :returns commit_or_rollback: Boolean.
514
+ :returns: The number of updated requests
514
515
  """
515
516
 
516
517
  request_id = tt_status_report.request_id
518
+ nb_updated = 0
517
519
  try:
518
520
  fields_to_update = tt_status_report.get_db_fields_to_update(session=session, logger=logger)
519
521
  if not fields_to_update:
@@ -522,23 +524,39 @@ def update_transfer_state(tt_status_report: TransferStatusReport, *, session: "S
522
524
  else:
523
525
  logger(logging.INFO, 'UPDATING REQUEST %s FOR %s with changes: %s' % (str(request_id), tt_status_report, fields_to_update))
524
526
 
525
- set_request_state(request_id, session=session, **fields_to_update)
526
- request = tt_status_report.request(session)
527
+ request = request_core.get_request(request_id, session=session)
528
+ updated = transition_request_state(request_id, request=request, session=session, **fields_to_update)
529
+
530
+ if not updated:
531
+ return nb_updated
532
+ nb_updated += 1
527
533
 
528
534
  if tt_status_report.state == RequestState.FAILED:
529
535
  if request_core.is_intermediate_hop(request):
530
- request_core.handle_failed_intermediate_hop(request, session=session)
531
-
536
+ nb_updated += request_core.handle_failed_intermediate_hop(request, session=session)
537
+
538
+ if tt_status_report.state:
539
+ stats_manager.observe(
540
+ src_rse_id=request['source_rse_id'],
541
+ dst_rse_id=request['dest_rse_id'],
542
+ activity=request['activity'],
543
+ state=tt_status_report.state,
544
+ file_size=request['bytes'],
545
+ submitted_at=request.get('submitted_at', None),
546
+ started_at=fields_to_update.get('started_at', None),
547
+ transferred_at=fields_to_update.get('transferred_at', None),
548
+ session=session,
549
+ )
532
550
  request_core.add_monitor_message(
533
551
  new_state=tt_status_report.state,
534
552
  request=request,
535
553
  additional_fields=tt_status_report.get_monitor_msg_fields(session=session, logger=logger),
536
554
  session=session
537
555
  )
538
- return True
556
+ return nb_updated
539
557
  except UnsupportedOperation as error:
540
558
  logger(logging.WARNING, "Request %s doesn't exist - Error: %s" % (request_id, str(error).replace('\n', '')))
541
- return False
559
+ return 0
542
560
  except Exception:
543
561
  logger(logging.CRITICAL, "Exception", exc_info=True)
544
562
 
@@ -549,7 +567,7 @@ def mark_transfer_lost(request, *, session: "Session", logger=logging.log):
549
567
  reason = "The FTS job lost"
550
568
 
551
569
  err_msg = request_core.get_transfer_error(new_state, reason)
552
- set_request_state(request['id'], state=new_state, external_id=request['external_id'], err_msg=err_msg, session=session, logger=logger)
570
+ transition_request_state(request['id'], state=new_state, external_id=request['external_id'], err_msg=err_msg, session=session, logger=logger)
553
571
 
554
572
  request_core.add_monitor_message(new_state=new_state, request=request, additional_fields={'reason': reason}, session=session)
555
573
 
@@ -583,21 +601,20 @@ def touch_transfer(external_host, transfer_id, *, session: "Session"):
583
601
  raise RucioException(error.args)
584
602
 
585
603
 
586
- @read_session
587
- def __create_transfer_definitions(
604
+ def _create_transfer_definitions(
588
605
  topology: "Topology",
589
606
  protocol_factory: ProtocolFactory,
590
607
  rws: RequestWithSources,
591
- sources: "list[RequestSource]",
608
+ sources: "Iterable[RequestSource]",
592
609
  max_sources: int,
593
- multi_source_sources: "list[RequestSource]",
610
+ multi_source_sources: "Iterable[RequestSource]",
594
611
  limit_dest_schemes: list[str],
595
612
  operation_src: str,
596
613
  operation_dest: str,
597
614
  domain: str,
598
615
  *,
599
616
  session: "Session",
600
- ) -> "dict[str, list[DirectTransferDefinition]]":
617
+ ) -> "dict[RseData, list[DirectTransfer]]":
601
618
  """
602
619
  Find the all paths from sources towards the destination of the given transfer request.
603
620
  Create the transfer definitions for each point-to-point transfer (multi-source, when possible)
@@ -615,17 +632,17 @@ def __create_transfer_definitions(
615
632
  hop_src_rse = hop['source_rse']
616
633
  hop_dst_rse = hop['dest_rse']
617
634
  src = RequestSource(
618
- rse_data=hop_src_rse,
635
+ rse=hop_src_rse,
619
636
  file_path=source.file_path if hop_src_rse == source.rse else None,
620
637
  ranking=source.ranking if hop_src_rse == source.rse else 0,
621
638
  distance=hop['cumulated_distance'] if hop_src_rse == source.rse else hop['hop_distance'],
622
639
  scheme=hop['source_scheme'],
623
640
  )
624
641
  dst = TransferDestination(
625
- rse_data=hop_dst_rse,
642
+ rse=hop_dst_rse,
626
643
  scheme=hop['dest_scheme'],
627
644
  )
628
- hop_definition = DirectTransferDefinition(
645
+ hop_definition = DirectTransferImplementation(
629
646
  source=src,
630
647
  destination=dst,
631
648
  operation_src=operation_src,
@@ -654,7 +671,7 @@ def __create_transfer_definitions(
654
671
  'allow_tape_source': True
655
672
  },
656
673
  previous_attempt_id=None,
657
- dest_rse_data=hop_dst_rse,
674
+ dest_rse=hop_dst_rse,
658
675
  account=rws.account,
659
676
  retry_count=0,
660
677
  priority=rws.priority,
@@ -664,7 +681,7 @@ def __create_transfer_definitions(
664
681
  )
665
682
 
666
683
  transfer_path.append(hop_definition)
667
- transfers_by_source[source.rse.id] = transfer_path
684
+ transfers_by_source[source.rse] = transfer_path
668
685
 
669
686
  # create multi-source transfers: add additional sources if possible
670
687
  for transfer_path in transfers_by_source.values():
@@ -703,7 +720,7 @@ def __create_transfer_definitions(
703
720
 
704
721
  transfer_path[0].sources.append(
705
722
  RequestSource(
706
- rse_data=source.rse,
723
+ rse=source.rse,
707
724
  file_path=source.file_path,
708
725
  ranking=source.ranking,
709
726
  distance=edge.cost,
@@ -714,35 +731,35 @@ def __create_transfer_definitions(
714
731
  return transfers_by_source
715
732
 
716
733
 
717
- def __create_stagein_definitions(
734
+ def _create_stagein_definitions(
718
735
  rws: RequestWithSources,
719
- sources: "list[RequestSource]",
736
+ sources: "Iterable[RequestSource]",
720
737
  limit_dest_schemes: list[str],
721
738
  operation_src: str,
722
739
  operation_dest: str,
723
740
  protocol_factory: ProtocolFactory,
724
- ) -> "dict[str, list[StageinTransferDefinition]]":
741
+ ) -> "dict[RseData, list[DirectTransfer]]":
725
742
  """
726
743
  for each source, create a single-hop transfer path with a one stageing definition inside
727
744
  """
728
745
  transfers_by_source = {
729
- source.rse.id: [
730
- StageinTransferDefinition(
746
+ source.rse: [
747
+ cast('DirectTransfer', StageinTransferImplementation(
731
748
  source=RequestSource(
732
- rse_data=source.rse,
749
+ rse=source.rse,
733
750
  file_path=source.file_path,
734
751
  url=source.url,
735
- scheme=limit_dest_schemes,
752
+ scheme=limit_dest_schemes, # type: ignore (list passed instead of single scheme)
736
753
  ),
737
754
  destination=TransferDestination(
738
- rse_data=rws.dest_rse,
739
- scheme=limit_dest_schemes,
755
+ rse=rws.dest_rse,
756
+ scheme=limit_dest_schemes, # type: ignore (list passed instead of single scheme)
740
757
  ),
741
758
  operation_src=operation_src,
742
759
  operation_dest=operation_dest,
743
760
  rws=rws,
744
761
  protocol_factory=protocol_factory,
745
- )
762
+ ))
746
763
 
747
764
  ]
748
765
  for source in sources
@@ -760,24 +777,15 @@ def get_dsn(scope, name, dsn):
760
777
  return 'other'
761
778
 
762
779
 
763
- def __filter_multihops_with_intermediate_tape(candidate_paths: "Iterable[list[DirectTransferDefinition]]") -> "Generator[list[DirectTransferDefinition]]":
764
- # Discard multihop transfers which contain a tape source as an intermediate hop
765
- for path in candidate_paths:
766
- if any(transfer.src.rse.is_tape_or_staging_required() for transfer in path[1:]):
767
- pass
768
- else:
769
- yield path
770
-
771
-
772
780
  def __compress_multihops(
773
- candidate_paths: "Iterable[list[DirectTransferDefinition]]",
781
+ paths_by_source: "Iterable[tuple[RequestSource, Sequence[DirectTransfer]]]",
774
782
  sources: "Iterable[RequestSource]",
775
- ) -> "Generator[list[DirectTransferDefinition]]":
783
+ ) -> "Iterator[tuple[RequestSource, Sequence[DirectTransfer]]]":
776
784
  # Compress multihop transfers which contain other sources as part of itself.
777
785
  # For example: multihop A->B->C and B is a source, compress A->B->C into B->C
778
786
  source_rses = {s.rse.id for s in sources}
779
787
  seen_source_rses = set()
780
- for path in candidate_paths:
788
+ for source, path in paths_by_source:
781
789
  if len(path) > 1:
782
790
  # find the index of the first hop starting from the end which is also a source. Path[0] will always be a source.
783
791
  last_source_idx = next((idx for idx, hop in reversed(list(enumerate(path))) if hop.src.rse.id in source_rses), (0, None))
@@ -788,27 +796,330 @@ def __compress_multihops(
788
796
  src_rse_id = path[0].src.rse.id
789
797
  if src_rse_id not in seen_source_rses:
790
798
  seen_source_rses.add(src_rse_id)
791
- yield path
792
-
793
-
794
- def __sort_paths(candidate_paths: "Iterable[list[DirectTransferDefinition]]") -> "Generator[list[DirectTransferDefinition]]":
795
-
796
- def __transfer_order_key(transfer_path):
797
- # Reduce the priority of the tape sources. If there are any disk sources,
798
- # they must fail twice (1 penalty + 1 disk preferred over tape) before a tape will even be tried
799
- source_ranking_penalty = 1 if transfer_path[0].src.rse.is_tape_or_staging_required() else 0
800
- # higher source_ranking first,
801
- # on equal source_ranking, prefer DISK over TAPE
802
- # on equal type, prefer lower distance
803
- # on equal distance, prefer single hop
804
- return (
805
- - transfer_path[0].src.ranking + source_ranking_penalty,
806
- transfer_path[0].src.rse.is_tape_or_staging_required(), # rely on the fact that False < True
807
- transfer_path[0].src.distance,
808
- len(transfer_path) > 1, # rely on the fact that False < True
809
- )
799
+ yield source, path
800
+
801
+
802
+ class TransferPathBuilder:
803
+ def __init__(
804
+ self,
805
+ topology: "Topology",
806
+ protocol_factory: ProtocolFactory,
807
+ max_sources: int,
808
+ preparer_mode: bool = False,
809
+ schemes: "Optional[list[str]]" = None,
810
+ failover_schemes: "Optional[list[str]]" = None,
811
+ requested_source_only: bool = False,
812
+ ):
813
+ self.failover_schemes = failover_schemes if failover_schemes is not None else []
814
+ self.schemes = schemes if schemes is not None else []
815
+ self.topology = topology
816
+ self.preparer_mode = preparer_mode
817
+ self.protocol_factory = protocol_factory
818
+ self.max_sources = max_sources
819
+ self.requested_source_only = requested_source_only
820
+
821
+ self.definition_by_request_id = {}
822
+
823
+ def build_or_return_cached(
824
+ self,
825
+ rws: RequestWithSources,
826
+ sources: "Iterable[RequestSource]",
827
+ *,
828
+ logger: "LoggerFunction" = logging.log,
829
+ session: "Session"
830
+ ) -> "Mapping[RseData, Sequence[DirectTransfer]]":
831
+ """
832
+ Warning: The function currently caches the result for the given request and returns it for later calls
833
+ with the same request id. As a result: it can return more (or less) sources than what is provided in the
834
+ `sources` argument. This is done for performance reasons. As of time of writing, this behavior is not problematic
835
+ for the callers of this method.
836
+ """
837
+ definition = self.definition_by_request_id.get(rws.request_id)
838
+ if definition:
839
+ return definition
840
+
841
+ transfer_schemes = self.schemes
842
+ if rws.previous_attempt_id and self.failover_schemes:
843
+ transfer_schemes = self.failover_schemes
844
+
845
+ candidate_sources = sources
846
+ if self.requested_source_only and rws.requested_source:
847
+ candidate_sources = [rws.requested_source] if rws.requested_source in sources else []
848
+
849
+ if rws.request_type == RequestType.STAGEIN:
850
+ definition = _create_stagein_definitions(
851
+ rws=rws,
852
+ sources=sources,
853
+ limit_dest_schemes=transfer_schemes,
854
+ operation_src='read',
855
+ operation_dest='write',
856
+ protocol_factory=self.protocol_factory
857
+ )
858
+ else:
859
+ definition = _create_transfer_definitions(
860
+ topology=self.topology,
861
+ rws=rws,
862
+ sources=candidate_sources,
863
+ max_sources=self.max_sources,
864
+ multi_source_sources=[] if self.preparer_mode else sources,
865
+ limit_dest_schemes=transfer_schemes,
866
+ operation_src='third_party_copy_read',
867
+ operation_dest='third_party_copy_write',
868
+ domain='wan',
869
+ protocol_factory=self.protocol_factory,
870
+ session=session
871
+ )
872
+ self.definition_by_request_id[rws.request_id] = definition
873
+ return definition
874
+
875
+
876
+ class _SkipSource:
877
+ pass
878
+
879
+
880
+ SKIP_SOURCE = _SkipSource()
881
+
882
+
883
+ class RequestRankingContext:
884
+ """
885
+ Helper class used by SourceRankingStrategy. It allows to store additional request-specific
886
+ context data and access it when handling a specific source of the given request.
887
+ """
888
+
889
+ def __init__(self, strategy: "SourceRankingStrategy", rws: "RequestWithSources"):
890
+ self.strategy = strategy
891
+ self.rws = rws
892
+
893
+ def apply(self, source: RequestSource) -> "int | _SkipSource":
894
+ verdict = self.strategy.apply(self, source)
895
+ if verdict is None:
896
+ verdict = sys.maxsize
897
+ return verdict
898
+
899
+
900
+ class SourceRankingStrategy:
901
+ """
902
+ Represents a source ranking strategy. Used to order the sources of a request and decide
903
+ which will be the actual source used for the transfer.
904
+
905
+ If filter_only is True, any value other than SKIP_SOURCE returned by apply() will be ignored.
906
+ """
907
+ filter_only: bool = False
908
+
909
+ def for_request(
910
+ self,
911
+ rws: RequestWithSources,
912
+ sources: "Iterable[RequestSource]",
913
+ *,
914
+ logger: "LoggerFunction" = logging.log,
915
+ session: "Session"
916
+ ) -> "RequestRankingContext":
917
+ return RequestRankingContext(self, rws)
918
+
919
+ def apply(self, ctx: RequestRankingContext, source: RequestSource) -> "Optional[int | _SkipSource]":
920
+ """
921
+ Normally, this function will be called indirectly, via self.for_request(...).apply(source).
922
+
923
+ It is expected to either return SKIP_SOURCE to signal that this source must be ignored;
924
+ or an integer which gives the cost of the given source under the current strategy
925
+ (smaller cost: higher priority).
926
+ If `None` is returned, it will be interpreted as sys.maxsize (i.e. very low priority).
927
+ This is done to avoid requiring an explicit integer in filter-only strategies.
928
+ """
929
+ pass
930
+
931
+ class _ClassNameDescriptor:
932
+ """
933
+ Automatically set the external_name of the strategy to the class name.
934
+ """
935
+ def __get__(self, obj, objtype=None):
936
+ if objtype is not None:
937
+ return objtype.__name__
938
+ return type(obj).__name__
939
+
940
+ external_name = _ClassNameDescriptor()
941
+
942
+
943
+ class SourceFilterStrategy(SourceRankingStrategy):
944
+ filter_only = True
945
+
810
946
 
811
- yield from sorted(candidate_paths, key=__transfer_order_key)
947
+ class EnforceSourceRSEExpression(SourceFilterStrategy):
948
+
949
+ class _RankingContext(RequestRankingContext):
950
+ def __init__(self, strategy: "SourceRankingStrategy", rws: "RequestWithSources", allowed_source_rses: "Optional[set[str]]"):
951
+ super().__init__(strategy, rws)
952
+ self.allowed_source_rses = allowed_source_rses
953
+
954
+ def for_request(
955
+ self,
956
+ rws: RequestWithSources,
957
+ sources: "Iterable[RequestSource]",
958
+ *,
959
+ logger: "LoggerFunction" = logging.log,
960
+ session: "Session"
961
+ ) -> "RequestRankingContext":
962
+ # parse source expression
963
+ allowed_source_rses = None
964
+ source_replica_expression = rws.attributes.get('source_replica_expression', None)
965
+ if source_replica_expression:
966
+ try:
967
+ parsed_rses = parse_expression(source_replica_expression, session=session)
968
+ except InvalidRSEExpression as error:
969
+ logger(logging.ERROR, "%s: Invalid RSE exception %s: %s", rws.request_id, source_replica_expression, str(error))
970
+ allowed_source_rses = set()
971
+ else:
972
+ allowed_source_rses = {x['id'] for x in parsed_rses}
973
+ return self._RankingContext(self, rws, allowed_source_rses)
974
+
975
+ def apply(self, ctx: RequestRankingContext, source: RequestSource) -> "Optional[int | _SkipSource]":
976
+ ctx = cast('EnforceSourceRSEExpression._RankingContext', ctx)
977
+ if ctx.allowed_source_rses is not None and source.rse.id not in ctx.allowed_source_rses:
978
+ return SKIP_SOURCE
979
+
980
+
981
+ class SkipRestrictedRSEs(SourceFilterStrategy):
982
+
983
+ def __init__(self, admin_accounts: "Optional[set[InternalAccount]]" = None):
984
+ super().__init__()
985
+ self.admin_accounts = admin_accounts if admin_accounts is not None else []
986
+
987
+ def apply(self, ctx: RequestRankingContext, source: RequestSource) -> "Optional[int | _SkipSource]":
988
+ if source.rse.attributes.get(RseAttr.RESTRICTED_READ) and ctx.rws.account not in self.admin_accounts:
989
+ return SKIP_SOURCE
990
+
991
+
992
+ class SkipBlocklistedRSEs(SourceFilterStrategy):
993
+
994
+ def __init__(self, topology: "Topology"):
995
+ super().__init__()
996
+ self.topology = topology
997
+
998
+ def apply(self, ctx: RequestRankingContext, source: RequestSource) -> "Optional[int | _SkipSource]":
999
+ # Ignore blocklisted RSEs
1000
+ if not source.rse.columns['availability_read'] and not self.topology.ignore_availability:
1001
+ return SKIP_SOURCE
1002
+
1003
+
1004
+ class EnforceStagingBuffer(SourceFilterStrategy):
1005
+ def apply(self, ctx: RequestRankingContext, source: RequestSource) -> "Optional[int | _SkipSource]":
1006
+ # For staging requests, the staging_buffer attribute must be correctly set
1007
+ if ctx.rws.request_type == RequestType.STAGEIN and source.rse.attributes.get(RseAttr.STAGING_BUFFER) != ctx.rws.dest_rse.name:
1008
+ return SKIP_SOURCE
1009
+
1010
+
1011
+ class RestrictTapeSources(SourceFilterStrategy):
1012
+ def apply(self, ctx: RequestRankingContext, source: RequestSource) -> "Optional[int | _SkipSource]":
1013
+ # Ignore tape sources if they are not desired
1014
+ if source.rse.is_tape_or_staging_required() and not ctx.rws.attributes.get("allow_tape_source", True):
1015
+ return SKIP_SOURCE
1016
+
1017
+
1018
+ class HighestAdjustedRankingFirst(SourceRankingStrategy):
1019
+ def apply(self, ctx: RequestRankingContext, source: RequestSource) -> "Optional[int | _SkipSource]":
1020
+ source_ranking_penalty = 1 if source.rse.is_tape_or_staging_required() else 0
1021
+ return - source.ranking + source_ranking_penalty
1022
+
1023
+
1024
+ class PreferDiskOverTape(SourceRankingStrategy):
1025
+ def apply(self, ctx: RequestRankingContext, source: RequestSource) -> "Optional[int | _SkipSource]":
1026
+ return int(source.rse.is_tape_or_staging_required()) # rely on the fact that False < True
1027
+
1028
+
1029
+ class PathDistance(SourceRankingStrategy):
1030
+
1031
+ class _RankingContext(RequestRankingContext):
1032
+ def __init__(self, strategy: "SourceRankingStrategy", rws: "RequestWithSources", paths_for_rws: "Mapping[RseData, Sequence[DirectTransfer]]"):
1033
+ super().__init__(strategy, rws)
1034
+ self.paths_for_rws = paths_for_rws
1035
+
1036
+ def __init__(self, transfer_path_builder: TransferPathBuilder):
1037
+ super().__init__()
1038
+ self.transfer_path_builder = transfer_path_builder
1039
+
1040
+ def for_request(
1041
+ self,
1042
+ rws: RequestWithSources,
1043
+ sources: "Iterable[RequestSource]",
1044
+ *,
1045
+ logger: "LoggerFunction" = logging.log,
1046
+ session: "Session"
1047
+ ) -> "RequestRankingContext":
1048
+ paths_for_rws = self.transfer_path_builder.build_or_return_cached(rws, sources, logger=logger, session=session)
1049
+ return PathDistance._RankingContext(self, rws, paths_for_rws)
1050
+
1051
+ def apply(self, ctx: RequestRankingContext, source: RequestSource) -> "Optional[int | _SkipSource]":
1052
+ path = cast('PathDistance._RankingContext', ctx).paths_for_rws.get(source.rse)
1053
+ if not path:
1054
+ return SKIP_SOURCE
1055
+ return path[0].src.distance
1056
+
1057
+
1058
+ class PreferSingleHop(PathDistance):
1059
+ def apply(self, ctx: RequestRankingContext, source: RequestSource) -> "Optional[int | _SkipSource]":
1060
+ path = cast('PathDistance._RankingContext', ctx).paths_for_rws.get(source.rse)
1061
+ if not path:
1062
+ return SKIP_SOURCE
1063
+ return int(len(path) > 1)
1064
+
1065
+
1066
+ class FailureRate(SourceRankingStrategy):
1067
+ """
1068
+ A source ranking strategy that ranks source nodes based on their failure rates for the past hour. Failure rate is
1069
+ calculated by dividing files failed by files attempted.
1070
+ """
1071
+ class _FailureRateStat:
1072
+ def __init__(self) -> None:
1073
+ self.files_done = 0
1074
+ self.files_failed = 0
1075
+
1076
+ def incorporate_stat(self, stat: "Mapping[str, int]") -> None:
1077
+ self.files_done += stat['files_done']
1078
+ self.files_failed += stat['files_failed']
1079
+
1080
+ def get_failure_rate(self) -> int:
1081
+ files_attempted = self.files_done + self.files_failed
1082
+
1083
+ # If no files have been sent yet, return failure rate as 0
1084
+ if files_attempted == 0:
1085
+ return 0
1086
+
1087
+ return int((self.files_failed / files_attempted) * 10000)
1088
+
1089
+ def __init__(self, stats_manager: "request_core.TransferStatsManager") -> None:
1090
+ super().__init__()
1091
+ self.source_stats = {}
1092
+
1093
+ for stat in stats_manager.load_totals(
1094
+ datetime.datetime.utcnow() - datetime.timedelta(hours=1),
1095
+ by_activity=False
1096
+ ):
1097
+ self.source_stats.setdefault(stat['src_rse_id'], self._FailureRateStat()).incorporate_stat(stat)
1098
+
1099
+ def apply(self, ctx: RequestRankingContext, source: RequestSource) -> "Optional[int | _SkipSource]":
1100
+ failure_rate = cast('FailureRate', ctx.strategy).source_stats.get(source.rse.id, self._FailureRateStat()).get_failure_rate()
1101
+ return failure_rate
1102
+
1103
+
1104
+ class SkipSchemeMissmatch(PathDistance):
1105
+ filter_only = True
1106
+
1107
+ def apply(self, ctx: RequestRankingContext, source: RequestSource) -> "Optional[int | _SkipSource]":
1108
+ path = cast('PathDistance._RankingContext', ctx).paths_for_rws.get(source.rse)
1109
+ # path == None means that there is no path;
1110
+ # path == [] means that a path exists (according to distances) but cannot be used (scheme mismatch)
1111
+ if path is not None and not path:
1112
+ return SKIP_SOURCE
1113
+
1114
+
1115
+ class SkipIntermediateTape(PathDistance):
1116
+ filter_only = True
1117
+
1118
+ def apply(self, ctx: RequestRankingContext, source: RequestSource) -> "Optional[int | _SkipSource]":
1119
+ # Discard multihop transfers which contain a tape source as an intermediate hop
1120
+ path = cast('PathDistance._RankingContext', ctx).paths_for_rws.get(source.rse)
1121
+ if path and any(transfer.src.rse.is_tape_or_staging_required() for transfer in path[1:]):
1122
+ return SKIP_SOURCE
812
1123
 
813
1124
 
814
1125
  @transactional_session
@@ -837,11 +1148,55 @@ def build_transfer_paths(
837
1148
 
838
1149
  Each path is a list of hops. Each hop is a transfer definition.
839
1150
  """
840
- if schemes is None:
841
- schemes = []
1151
+ transfer_path_builder = TransferPathBuilder(
1152
+ topology=topology,
1153
+ schemes=schemes,
1154
+ failover_schemes=failover_schemes,
1155
+ protocol_factory=protocol_factory,
1156
+ max_sources=max_sources,
1157
+ preparer_mode=preparer_mode,
1158
+ requested_source_only=requested_source_only,
1159
+ )
842
1160
 
843
- if failover_schemes is None:
844
- failover_schemes = []
1161
+ stats_manager = request_core.TransferStatsManager()
1162
+
1163
+ available_strategies = {
1164
+ EnforceSourceRSEExpression.external_name: lambda: EnforceSourceRSEExpression(),
1165
+ SkipBlocklistedRSEs.external_name: lambda: SkipBlocklistedRSEs(topology=topology),
1166
+ SkipRestrictedRSEs.external_name: lambda: SkipRestrictedRSEs(admin_accounts=admin_accounts),
1167
+ EnforceStagingBuffer.external_name: lambda: EnforceStagingBuffer(),
1168
+ RestrictTapeSources.external_name: lambda: RestrictTapeSources(),
1169
+ SkipSchemeMissmatch.external_name: lambda: SkipSchemeMissmatch(transfer_path_builder=transfer_path_builder),
1170
+ SkipIntermediateTape.external_name: lambda: SkipIntermediateTape(transfer_path_builder=transfer_path_builder),
1171
+ HighestAdjustedRankingFirst.external_name: lambda: HighestAdjustedRankingFirst(),
1172
+ PreferDiskOverTape.external_name: lambda: PreferDiskOverTape(),
1173
+ PathDistance.external_name: lambda: PathDistance(transfer_path_builder=transfer_path_builder),
1174
+ PreferSingleHop.external_name: lambda: PreferSingleHop(transfer_path_builder=transfer_path_builder),
1175
+ FailureRate.external_name: lambda: FailureRate(stats_manager=stats_manager),
1176
+ }
1177
+
1178
+ default_strategies = [
1179
+ EnforceSourceRSEExpression.external_name,
1180
+ SkipBlocklistedRSEs.external_name,
1181
+ SkipRestrictedRSEs.external_name,
1182
+ EnforceStagingBuffer.external_name,
1183
+ RestrictTapeSources.external_name,
1184
+ # Without the SkipSchemeMissmatch strategy, requests will never be transitioned to the
1185
+ # RequestState.MISMATCH_SCHEME state. It _MUST_ be placed before the other Path-based strategies.
1186
+ SkipSchemeMissmatch.external_name,
1187
+ SkipIntermediateTape.external_name,
1188
+ HighestAdjustedRankingFirst.external_name,
1189
+ PreferDiskOverTape.external_name,
1190
+ PathDistance.external_name,
1191
+ PreferSingleHop.external_name,
1192
+ ]
1193
+ strategy_names = config_get_list('transfers', 'source_ranking_strategies', default=default_strategies)
1194
+
1195
+ try:
1196
+ strategies = list(available_strategies[name]() for name in strategy_names)
1197
+ except KeyError:
1198
+ logger(logging.ERROR, "One of the configured source_ranking_strategies doesn't exist %s", strategy_names, exc_info=True)
1199
+ raise
845
1200
 
846
1201
  if admin_accounts is None:
847
1202
  admin_accounts = set()
@@ -860,10 +1215,6 @@ def build_transfer_paths(
860
1215
  for source in all_sources:
861
1216
  source.rse.ensure_loaded(load_name=True, load_info=True, load_attributes=True, load_columns=True, session=session)
862
1217
 
863
- transfer_schemes = schemes
864
- if rws.previous_attempt_id and failover_schemes:
865
- transfer_schemes = failover_schemes
866
-
867
1218
  # Assume request doesn't have any sources. Will be removed later if sources are found.
868
1219
  reqs_no_source.add(rws.request_id)
869
1220
  if not all_sources:
@@ -881,7 +1232,7 @@ def build_transfer_paths(
881
1232
  if not (topology.ignore_availability or rws.dest_rse.columns['availability_write']):
882
1233
  logger(logging.WARNING, '%s: dst RSE is blocked for write. Will skip the submission of new jobs', rws.request_id)
883
1234
  continue
884
- if rws.account not in admin_accounts and rws.dest_rse.attributes.get('restricted_write'):
1235
+ if rws.account not in admin_accounts and rws.dest_rse.attributes.get(RseAttr.RESTRICTED_WRITE):
885
1236
  logger(logging.WARNING, '%s: dst RSE is restricted for write. Will skip the submission', rws.request_id)
886
1237
  continue
887
1238
 
@@ -892,122 +1243,55 @@ def build_transfer_paths(
892
1243
  reqs_no_source.remove(rws.request_id)
893
1244
  continue
894
1245
 
895
- # parse source expression
896
- source_replica_expression = rws.attributes.get('source_replica_expression', None)
897
- allowed_source_rses = None
898
- if source_replica_expression:
899
- try:
900
- parsed_rses = parse_expression(source_replica_expression, session=session)
901
- except InvalidRSEExpression as error:
902
- logger(logging.ERROR, "%s: Invalid RSE exception %s: %s", rws.request_id, source_replica_expression, str(error))
903
- continue
904
- else:
905
- allowed_source_rses = [x['id'] for x in parsed_rses]
906
-
907
- filtered_sources = all_sources
908
- # Only keep allowed sources
909
- if allowed_source_rses is not None:
910
- filtered_sources = filter(lambda s: s.rse.id in allowed_source_rses, filtered_sources)
911
- filtered_sources = filter(lambda s: s.rse.name is not None, filtered_sources)
912
- if rws.account not in admin_accounts:
913
- filtered_sources = filter(lambda s: not s.rse.attributes.get('restricted_read'), filtered_sources)
914
- # Ignore blocklisted RSEs
915
- if not topology.ignore_availability:
916
- filtered_sources = filter(lambda s: s.rse.columns['availability_read'], filtered_sources)
917
- # For staging requests, the staging_buffer attribute must be correctly set
918
- if rws.request_type == RequestType.STAGEIN:
919
- filtered_sources = filter(lambda s: s.rse.attributes.get('staging_buffer') == rws.dest_rse.name, filtered_sources)
920
- # Ignore tape sources if they are not desired
921
- filtered_sources = list(filtered_sources)
922
- had_tape_sources = len(filtered_sources) > 0
923
- if not rws.attributes.get("allow_tape_source", True):
924
- filtered_sources = filter(lambda s: not s.rse.is_tape_or_staging_required(), filtered_sources)
925
-
926
- filtered_sources = list(filtered_sources)
927
- filtered_rses_log = ''
928
- if len(all_sources) != len(filtered_sources):
929
- filtered_rses = list(set(s.rse.name for s in all_sources).difference(s.rse.name for s in filtered_sources))
930
- filtered_rses_log = '; %d dropped by filter: ' % (len(all_sources) - len(filtered_sources))
931
- filtered_rses_log += ','.join(filtered_rses[:num_sources_in_logs])
932
- if len(filtered_rses) > num_sources_in_logs:
933
- filtered_rses_log += '... and %d others' % (len(filtered_rses) - num_sources_in_logs)
934
- candidate_paths = []
935
-
936
- candidate_sources = filtered_sources
937
- if requested_source_only and rws.requested_source:
938
- candidate_sources = [rws.requested_source] if rws.requested_source in filtered_sources else []
939
-
940
- if rws.request_type == RequestType.STAGEIN:
941
- paths = __create_stagein_definitions(rws=rws,
942
- sources=candidate_sources,
943
- limit_dest_schemes=transfer_schemes,
944
- operation_src='read',
945
- operation_dest='write',
946
- protocol_factory=protocol_factory)
947
- else:
948
- paths = __create_transfer_definitions(topology=topology,
949
- rws=rws,
950
- sources=candidate_sources,
951
- max_sources=max_sources,
952
- multi_source_sources=[] if preparer_mode else filtered_sources,
953
- limit_dest_schemes=[],
954
- operation_src='third_party_copy_read',
955
- operation_dest='third_party_copy_write',
956
- domain='wan',
957
- protocol_factory=protocol_factory,
958
- session=session)
959
-
960
- sources_without_path = []
961
- any_source_had_scheme_mismatch = False
962
- for source in candidate_sources:
963
- transfer_path = paths.get(source.rse.id)
964
- if transfer_path is None:
965
- logger(logging.WARNING, "%s: no path from %s to %s", rws.request_id, source.rse, rws.dest_rse)
966
- sources_without_path.append(source.rse.name)
967
- continue
968
- if not transfer_path:
969
- any_source_had_scheme_mismatch = True
970
- logger(logging.WARNING, "%s: no matching protocol between %s and %s", rws.request_id, source.rse, rws.dest_rse)
971
- sources_without_path.append(source.rse.name)
972
- continue
973
-
974
- if len(transfer_path) > 1:
975
- logger(logging.DEBUG, '%s: From %s to %s requires multihop: %s', rws.request_id, source.rse, rws.dest_rse, transfer_path_str(transfer_path))
976
-
977
- candidate_paths.append(transfer_path)
978
-
979
- if len(candidate_sources) != len(candidate_paths):
980
- logger(logging.DEBUG, '%s: Sources after path computation: %s', rws.request_id, [str(path[0].src.rse) for path in candidate_paths])
981
-
982
- sources_without_path_log = ''
983
- if sources_without_path:
984
- sources_without_path_log = '; %d dropped due to missing path: ' % len(sources_without_path)
985
- sources_without_path_log += ','.join(sources_without_path[:num_sources_in_logs])
986
- if len(sources_without_path) > num_sources_in_logs:
987
- sources_without_path_log += '... and %d others' % (len(sources_without_path) - num_sources_in_logs)
988
-
989
- candidate_paths = __filter_multihops_with_intermediate_tape(candidate_paths)
1246
+ # For each strategy name, gives the sources which were rejected by it
1247
+ rejected_sources = defaultdict(list)
1248
+ # Cost of each accepted source (lists of ordered costs: one for each ranking strategy)
1249
+ cost_vectors = {s: [] for s in rws.sources}
1250
+ for strategy in strategies:
1251
+ sources = list(cost_vectors)
1252
+ if not sources:
1253
+ # All sources where filtered by previous strategies. It's worthless to continue.
1254
+ break
1255
+ rws_strategy = strategy.for_request(rws, sources, logger=logger, session=session)
1256
+ for source in sources:
1257
+ verdict = rws_strategy.apply(source)
1258
+ if verdict is SKIP_SOURCE:
1259
+ rejected_sources[strategy.external_name].append(source)
1260
+ cost_vectors.pop(source)
1261
+ elif not strategy.filter_only:
1262
+ cost_vectors[source].append(verdict)
1263
+
1264
+ transfers_by_rse = transfer_path_builder.build_or_return_cached(rws, cost_vectors, logger=logger, session=session)
1265
+ candidate_paths = ((s, transfers_by_rse[s.rse]) for s, _ in sorted(cost_vectors.items(), key=operator.itemgetter(1)))
990
1266
  if not preparer_mode:
991
1267
  candidate_paths = __compress_multihops(candidate_paths, all_sources)
992
- candidate_paths = list(__sort_paths(candidate_paths))
1268
+ candidate_paths = list(candidate_paths)
993
1269
 
994
- ordered_sources_log = ','.join(('multihop: ' if len(path) > 1 else '') + '{}:{}:{}'.format(path[0].src.rse, path[0].src.ranking, path[0].src.distance)
995
- for path in candidate_paths[:num_sources_in_logs])
1270
+ ordered_sources_log = ', '.join(
1271
+ f"{s.rse}:{':'.join(str(e) for e in cost_vectors[s])}"
1272
+ f"{'(actual source ' + str(path[0].src.rse) + ')' if s.rse != path[0].src.rse else ''}"
1273
+ f"{'(multihop)' if len(path) > 1 else ''}"
1274
+ for s, path in candidate_paths[:num_sources_in_logs]
1275
+ )
996
1276
  if len(candidate_paths) > num_sources_in_logs:
997
1277
  ordered_sources_log += '... and %d others' % (len(candidate_paths) - num_sources_in_logs)
998
-
999
- logger(logging.INFO, '%s: %d ordered sources: %s%s%s', rws, len(candidate_paths),
1000
- ordered_sources_log, filtered_rses_log, sources_without_path_log)
1278
+ filtered_rses_log = ''
1279
+ for strategy_name, sources in rejected_sources.items():
1280
+ filtered_rses_log += f'; {len(sources)} dropped by strategy "{strategy_name}": '
1281
+ filtered_rses_log += ','.join(str(s.rse) for s in sources[:num_sources_in_logs])
1282
+ if len(sources) > num_sources_in_logs:
1283
+ filtered_rses_log += '... and %d others' % (len(sources) - num_sources_in_logs)
1284
+ logger(logging.INFO, '%s: %d ordered sources: %s%s', rws, len(candidate_paths), ordered_sources_log, filtered_rses_log)
1001
1285
 
1002
1286
  if not candidate_paths:
1003
1287
  # It can happen that some sources are skipped because they are TAPE, and others because
1004
1288
  # of scheme mismatch. However, we can only have one state in the database. I picked to
1005
1289
  # prioritize setting only_tape_source without any particular reason.
1006
- if had_tape_sources and not filtered_sources:
1290
+ if RestrictTapeSources.external_name in rejected_sources:
1007
1291
  logger(logging.DEBUG, '%s: Only tape sources found' % rws.request_id)
1008
1292
  reqs_only_tape_source.add(rws.request_id)
1009
1293
  reqs_no_source.remove(rws.request_id)
1010
- elif any_source_had_scheme_mismatch:
1294
+ elif SkipSchemeMissmatch.external_name in rejected_sources:
1011
1295
  logger(logging.DEBUG, '%s: Scheme mismatch detected' % rws.request_id)
1012
1296
  reqs_scheme_mismatch.add(rws.request_id)
1013
1297
  reqs_no_source.remove(rws.request_id)
@@ -1015,7 +1299,7 @@ def build_transfer_paths(
1015
1299
  logger(logging.DEBUG, '%s: No candidate path found' % rws.request_id)
1016
1300
  continue
1017
1301
 
1018
- candidate_paths_by_request_id[rws.request_id] = candidate_paths
1302
+ candidate_paths_by_request_id[rws.request_id] = [path for _, path in candidate_paths]
1019
1303
  reqs_no_source.remove(rws.request_id)
1020
1304
 
1021
1305
  return candidate_paths_by_request_id, reqs_no_source, reqs_scheme_mismatch, reqs_only_tape_source, reqs_unsupported_transfertool
@@ -1104,7 +1388,7 @@ def cancel_transfer(transfertool_obj, transfer_id):
1104
1388
 
1105
1389
  @transactional_session
1106
1390
  def prepare_transfers(
1107
- candidate_paths_by_request_id: "dict[str, list[list[DirectTransferDefinition]]]",
1391
+ candidate_paths_by_request_id: "dict[str, list[list[DirectTransfer]]]",
1108
1392
  logger: "LoggerFunction" = logging.log,
1109
1393
  transfertools: "Optional[list[str]]" = None,
1110
1394
  *,
@@ -1142,7 +1426,7 @@ def prepare_transfers(
1142
1426
  logger(logging.WARNING, '%s: all available sources were filtered', rws)
1143
1427
  continue
1144
1428
 
1145
- update_dict: dict[Any, Any] = {
1429
+ update_dict: "dict[Any, Any]" = {
1146
1430
  models.Request.state.name: _throttler_request_state(
1147
1431
  activity=rws.activity,
1148
1432
  source_rse=selected_source.rse,