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.
- rucio/__init__.py +0 -1
- rucio/alembicrevision.py +1 -2
- rucio/client/__init__.py +0 -1
- rucio/client/accountclient.py +45 -25
- rucio/client/accountlimitclient.py +37 -9
- rucio/client/baseclient.py +199 -154
- rucio/client/client.py +2 -3
- rucio/client/configclient.py +19 -6
- rucio/client/credentialclient.py +9 -4
- rucio/client/didclient.py +238 -63
- rucio/client/diracclient.py +13 -5
- rucio/client/downloadclient.py +162 -51
- rucio/client/exportclient.py +4 -4
- rucio/client/fileclient.py +3 -4
- rucio/client/importclient.py +4 -4
- rucio/client/lifetimeclient.py +21 -5
- rucio/client/lockclient.py +18 -8
- rucio/client/{metaclient.py → metaconventionsclient.py} +18 -15
- rucio/client/pingclient.py +0 -1
- rucio/client/replicaclient.py +15 -5
- rucio/client/requestclient.py +35 -19
- rucio/client/rseclient.py +133 -51
- rucio/client/ruleclient.py +29 -22
- rucio/client/scopeclient.py +8 -6
- rucio/client/subscriptionclient.py +47 -35
- rucio/client/touchclient.py +8 -4
- rucio/client/uploadclient.py +166 -82
- rucio/common/__init__.py +0 -1
- rucio/common/cache.py +4 -4
- rucio/common/config.py +52 -47
- rucio/common/constants.py +69 -2
- rucio/common/constraints.py +0 -1
- rucio/common/didtype.py +24 -22
- rucio/common/dumper/__init__.py +70 -41
- rucio/common/dumper/consistency.py +26 -22
- rucio/common/dumper/data_models.py +16 -23
- rucio/common/dumper/path_parsing.py +0 -1
- rucio/common/exception.py +281 -222
- rucio/common/extra.py +0 -1
- rucio/common/logging.py +54 -38
- rucio/common/pcache.py +122 -101
- rucio/common/plugins.py +153 -0
- rucio/common/policy.py +4 -4
- rucio/common/schema/__init__.py +17 -10
- rucio/common/schema/atlas.py +7 -5
- rucio/common/schema/belleii.py +7 -5
- rucio/common/schema/domatpc.py +7 -5
- rucio/common/schema/escape.py +7 -5
- rucio/common/schema/generic.py +8 -6
- rucio/common/schema/generic_multi_vo.py +7 -5
- rucio/common/schema/icecube.py +7 -5
- rucio/common/stomp_utils.py +0 -1
- rucio/common/stopwatch.py +0 -1
- rucio/common/test_rucio_server.py +2 -2
- rucio/common/types.py +262 -17
- rucio/common/utils.py +743 -451
- rucio/core/__init__.py +0 -1
- rucio/core/account.py +99 -29
- rucio/core/account_counter.py +89 -24
- rucio/core/account_limit.py +90 -24
- rucio/core/authentication.py +86 -29
- rucio/core/config.py +108 -38
- rucio/core/credential.py +14 -7
- rucio/core/did.py +680 -782
- rucio/core/did_meta_plugins/__init__.py +8 -6
- rucio/core/did_meta_plugins/did_column_meta.py +17 -12
- rucio/core/did_meta_plugins/did_meta_plugin_interface.py +60 -11
- rucio/core/did_meta_plugins/filter_engine.py +90 -50
- rucio/core/did_meta_plugins/json_meta.py +41 -16
- rucio/core/did_meta_plugins/mongo_meta.py +25 -8
- rucio/core/did_meta_plugins/postgres_meta.py +3 -4
- rucio/core/dirac.py +46 -17
- rucio/core/distance.py +66 -43
- rucio/core/exporter.py +5 -5
- rucio/core/heartbeat.py +181 -81
- rucio/core/identity.py +22 -12
- rucio/core/importer.py +23 -12
- rucio/core/lifetime_exception.py +32 -32
- rucio/core/lock.py +244 -142
- rucio/core/message.py +79 -38
- rucio/core/{meta.py → meta_conventions.py} +57 -44
- rucio/core/monitor.py +19 -13
- rucio/core/naming_convention.py +68 -27
- rucio/core/nongrid_trace.py +17 -5
- rucio/core/oidc.py +151 -29
- rucio/core/permission/__init__.py +18 -6
- rucio/core/permission/atlas.py +50 -35
- rucio/core/permission/belleii.py +6 -5
- rucio/core/permission/escape.py +8 -6
- rucio/core/permission/generic.py +82 -80
- rucio/core/permission/generic_multi_vo.py +9 -7
- rucio/core/quarantined_replica.py +91 -58
- rucio/core/replica.py +1303 -772
- rucio/core/replica_sorter.py +10 -12
- rucio/core/request.py +1133 -285
- rucio/core/rse.py +142 -102
- rucio/core/rse_counter.py +49 -18
- rucio/core/rse_expression_parser.py +6 -7
- rucio/core/rse_selector.py +41 -16
- rucio/core/rule.py +1538 -474
- rucio/core/rule_grouping.py +213 -68
- rucio/core/scope.py +50 -22
- rucio/core/subscription.py +92 -44
- rucio/core/topology.py +66 -24
- rucio/core/trace.py +42 -28
- rucio/core/transfer.py +543 -259
- rucio/core/vo.py +36 -18
- rucio/core/volatile_replica.py +59 -32
- rucio/daemons/__init__.py +0 -1
- rucio/daemons/abacus/__init__.py +0 -1
- rucio/daemons/abacus/account.py +29 -19
- rucio/daemons/abacus/collection_replica.py +21 -10
- rucio/daemons/abacus/rse.py +22 -12
- rucio/daemons/atropos/__init__.py +0 -1
- rucio/daemons/atropos/atropos.py +1 -2
- rucio/daemons/auditor/__init__.py +56 -28
- rucio/daemons/auditor/hdfs.py +17 -6
- rucio/daemons/auditor/srmdumps.py +116 -45
- rucio/daemons/automatix/__init__.py +0 -1
- rucio/daemons/automatix/automatix.py +30 -18
- rucio/daemons/badreplicas/__init__.py +0 -1
- rucio/daemons/badreplicas/minos.py +29 -18
- rucio/daemons/badreplicas/minos_temporary_expiration.py +5 -7
- rucio/daemons/badreplicas/necromancer.py +9 -13
- rucio/daemons/bb8/__init__.py +0 -1
- rucio/daemons/bb8/bb8.py +10 -13
- rucio/daemons/bb8/common.py +151 -154
- rucio/daemons/bb8/nuclei_background_rebalance.py +15 -9
- rucio/daemons/bb8/t2_background_rebalance.py +15 -8
- rucio/daemons/c3po/__init__.py +0 -1
- rucio/daemons/c3po/algorithms/__init__.py +0 -1
- rucio/daemons/c3po/algorithms/simple.py +8 -5
- rucio/daemons/c3po/algorithms/t2_free_space.py +10 -7
- rucio/daemons/c3po/algorithms/t2_free_space_only_pop.py +10 -7
- rucio/daemons/c3po/algorithms/t2_free_space_only_pop_with_network.py +30 -15
- rucio/daemons/c3po/c3po.py +81 -52
- rucio/daemons/c3po/collectors/__init__.py +0 -1
- rucio/daemons/c3po/collectors/agis.py +17 -17
- rucio/daemons/c3po/collectors/free_space.py +32 -13
- rucio/daemons/c3po/collectors/jedi_did.py +14 -5
- rucio/daemons/c3po/collectors/mock_did.py +11 -6
- rucio/daemons/c3po/collectors/network_metrics.py +12 -4
- rucio/daemons/c3po/collectors/workload.py +21 -19
- rucio/daemons/c3po/utils/__init__.py +0 -1
- rucio/daemons/c3po/utils/dataset_cache.py +15 -5
- rucio/daemons/c3po/utils/expiring_dataset_cache.py +16 -5
- rucio/daemons/c3po/utils/expiring_list.py +6 -7
- rucio/daemons/c3po/utils/popularity.py +5 -2
- rucio/daemons/c3po/utils/timeseries.py +25 -12
- rucio/daemons/cache/__init__.py +0 -1
- rucio/daemons/cache/consumer.py +21 -15
- rucio/daemons/common.py +42 -18
- rucio/daemons/conveyor/__init__.py +0 -1
- rucio/daemons/conveyor/common.py +69 -37
- rucio/daemons/conveyor/finisher.py +83 -46
- rucio/daemons/conveyor/poller.py +101 -69
- rucio/daemons/conveyor/preparer.py +35 -28
- rucio/daemons/conveyor/receiver.py +64 -21
- rucio/daemons/conveyor/stager.py +33 -28
- rucio/daemons/conveyor/submitter.py +71 -47
- rucio/daemons/conveyor/throttler.py +99 -35
- rucio/daemons/follower/__init__.py +0 -1
- rucio/daemons/follower/follower.py +12 -8
- rucio/daemons/hermes/__init__.py +0 -1
- rucio/daemons/hermes/hermes.py +57 -21
- rucio/daemons/judge/__init__.py +0 -1
- rucio/daemons/judge/cleaner.py +27 -17
- rucio/daemons/judge/evaluator.py +31 -18
- rucio/daemons/judge/injector.py +31 -23
- rucio/daemons/judge/repairer.py +28 -18
- rucio/daemons/oauthmanager/__init__.py +0 -1
- rucio/daemons/oauthmanager/oauthmanager.py +7 -8
- rucio/daemons/reaper/__init__.py +0 -1
- rucio/daemons/reaper/dark_reaper.py +15 -9
- rucio/daemons/reaper/reaper.py +109 -67
- rucio/daemons/replicarecoverer/__init__.py +0 -1
- rucio/daemons/replicarecoverer/suspicious_replica_recoverer.py +255 -116
- rucio/{api → daemons/rsedecommissioner}/__init__.py +0 -1
- rucio/daemons/rsedecommissioner/config.py +81 -0
- rucio/daemons/rsedecommissioner/profiles/__init__.py +24 -0
- rucio/daemons/rsedecommissioner/profiles/atlas.py +60 -0
- rucio/daemons/rsedecommissioner/profiles/generic.py +451 -0
- rucio/daemons/rsedecommissioner/profiles/types.py +92 -0
- rucio/daemons/rsedecommissioner/rse_decommissioner.py +280 -0
- rucio/daemons/storage/__init__.py +0 -1
- rucio/daemons/storage/consistency/__init__.py +0 -1
- rucio/daemons/storage/consistency/actions.py +152 -59
- rucio/daemons/tracer/__init__.py +0 -1
- rucio/daemons/tracer/kronos.py +47 -24
- rucio/daemons/transmogrifier/__init__.py +0 -1
- rucio/daemons/transmogrifier/transmogrifier.py +35 -26
- rucio/daemons/undertaker/__init__.py +0 -1
- rucio/daemons/undertaker/undertaker.py +10 -10
- rucio/db/__init__.py +0 -1
- rucio/db/sqla/__init__.py +16 -2
- rucio/db/sqla/constants.py +10 -1
- rucio/db/sqla/migrate_repo/__init__.py +0 -1
- rucio/db/sqla/migrate_repo/env.py +0 -1
- rucio/db/sqla/migrate_repo/versions/01eaf73ab656_add_new_rule_notification_state_progress.py +0 -1
- rucio/db/sqla/migrate_repo/versions/0437a40dbfd1_add_eol_at_in_rules.py +0 -3
- rucio/db/sqla/migrate_repo/versions/0f1adb7a599a_create_transfer_hops_table.py +1 -3
- rucio/db/sqla/migrate_repo/versions/102efcf145f4_added_stuck_at_column_to_rules.py +0 -3
- rucio/db/sqla/migrate_repo/versions/13d4f70c66a9_introduce_transfer_limits.py +1 -3
- rucio/db/sqla/migrate_repo/versions/140fef722e91_cleanup_distances_table.py +1 -3
- rucio/db/sqla/migrate_repo/versions/14ec5aeb64cf_add_request_external_host.py +0 -3
- rucio/db/sqla/migrate_repo/versions/156fb5b5a14_add_request_type_to_requests_idx.py +1 -4
- rucio/db/sqla/migrate_repo/versions/1677d4d803c8_split_rse_availability_into_multiple.py +0 -1
- rucio/db/sqla/migrate_repo/versions/16a0aca82e12_create_index_on_table_replicas_path.py +0 -2
- rucio/db/sqla/migrate_repo/versions/1803333ac20f_adding_provenance_and_phys_group.py +0 -1
- rucio/db/sqla/migrate_repo/versions/1a29d6a9504c_add_didtype_chck_to_requests.py +0 -1
- rucio/db/sqla/migrate_repo/versions/1a80adff031a_create_index_on_rules_hist_recent.py +0 -2
- rucio/db/sqla/migrate_repo/versions/1c45d9730ca6_increase_identity_length.py +0 -1
- rucio/db/sqla/migrate_repo/versions/1d1215494e95_add_quarantined_replicas_table.py +1 -3
- rucio/db/sqla/migrate_repo/versions/1d96f484df21_asynchronous_rules_and_rule_approval.py +0 -1
- rucio/db/sqla/migrate_repo/versions/1f46c5f240ac_add_bytes_column_to_bad_replicas.py +0 -3
- rucio/db/sqla/migrate_repo/versions/1fc15ab60d43_add_message_history_table.py +0 -1
- rucio/db/sqla/migrate_repo/versions/2190e703eb6e_move_rse_settings_to_rse_attributes.py +1 -2
- rucio/db/sqla/migrate_repo/versions/21d6b9dc9961_add_mismatch_scheme_state_to_requests.py +0 -1
- rucio/db/sqla/migrate_repo/versions/22cf51430c78_add_availability_column_to_table_rses.py +0 -3
- rucio/db/sqla/migrate_repo/versions/22d887e4ec0a_create_sources_table.py +1 -3
- rucio/db/sqla/migrate_repo/versions/25821a8a45a3_remove_unique_constraint_on_requests.py +1 -4
- rucio/db/sqla/migrate_repo/versions/25fc855625cf_added_unique_constraint_to_rules.py +0 -2
- rucio/db/sqla/migrate_repo/versions/269fee20dee9_add_repair_cnt_to_locks.py +0 -3
- rucio/db/sqla/migrate_repo/versions/271a46ea6244_add_ignore_availability_column_to_rules.py +0 -3
- rucio/db/sqla/migrate_repo/versions/277b5fbb41d3_switch_heartbeats_executable.py +1 -2
- rucio/db/sqla/migrate_repo/versions/27e3a68927fb_remove_replicas_tombstone_and_replicas_.py +0 -1
- rucio/db/sqla/migrate_repo/versions/2854cd9e168_added_rule_id_column.py +0 -1
- rucio/db/sqla/migrate_repo/versions/295289b5a800_processed_by_and__at_in_requests.py +0 -2
- rucio/db/sqla/migrate_repo/versions/2962ece31cf4_add_nbaccesses_column_in_the_did_table.py +0 -3
- rucio/db/sqla/migrate_repo/versions/2af3291ec4c_added_replicas_history_table.py +1 -3
- rucio/db/sqla/migrate_repo/versions/2b69addda658_add_columns_for_third_party_copy_read_.py +0 -2
- rucio/db/sqla/migrate_repo/versions/2b8e7bcb4783_add_config_table.py +1 -4
- rucio/db/sqla/migrate_repo/versions/2ba5229cb54c_add_submitted_at_to_requests_table.py +0 -3
- rucio/db/sqla/migrate_repo/versions/2cbee484dcf9_added_column_volume_to_rse_transfer_.py +1 -4
- rucio/db/sqla/migrate_repo/versions/2edee4a83846_add_source_to_requests_and_requests_.py +0 -1
- rucio/db/sqla/migrate_repo/versions/2eef46be23d4_change_tokens_pk.py +1 -3
- rucio/db/sqla/migrate_repo/versions/2f648fc909f3_index_in_rule_history_on_scope_name.py +0 -2
- rucio/db/sqla/migrate_repo/versions/3082b8cef557_add_naming_convention_table_and_closed_.py +1 -3
- rucio/db/sqla/migrate_repo/versions/30fa38b6434e_add_index_on_service_column_in_the_message_table.py +1 -3
- rucio/db/sqla/migrate_repo/versions/3152492b110b_added_staging_area_column.py +1 -2
- rucio/db/sqla/migrate_repo/versions/32c7d2783f7e_create_bad_replicas_table.py +1 -3
- rucio/db/sqla/migrate_repo/versions/3345511706b8_replicas_table_pk_definition_is_in_.py +1 -3
- rucio/db/sqla/migrate_repo/versions/35ef10d1e11b_change_index_on_table_requests.py +0 -2
- rucio/db/sqla/migrate_repo/versions/379a19b5332d_create_rse_limits_table.py +1 -3
- rucio/db/sqla/migrate_repo/versions/384b96aa0f60_created_rule_history_tables.py +2 -3
- rucio/db/sqla/migrate_repo/versions/3ac1660a1a72_extend_distance_table.py +0 -3
- rucio/db/sqla/migrate_repo/versions/3ad36e2268b0_create_collection_replicas_updates_table.py +1 -4
- rucio/db/sqla/migrate_repo/versions/3c9df354071b_extend_waiting_request_state.py +0 -1
- rucio/db/sqla/migrate_repo/versions/3d9813fab443_add_a_new_state_lost_in_badfilesstatus.py +0 -1
- rucio/db/sqla/migrate_repo/versions/40ad39ce3160_add_transferred_at_to_requests_table.py +0 -3
- rucio/db/sqla/migrate_repo/versions/4207be2fd914_add_notification_column_to_rules.py +0 -1
- rucio/db/sqla/migrate_repo/versions/42db2617c364_create_index_on_requests_external_id.py +0 -2
- rucio/db/sqla/migrate_repo/versions/436827b13f82_added_column_activity_to_table_requests.py +0 -3
- rucio/db/sqla/migrate_repo/versions/44278720f774_update_requests_typ_sta_upd_idx_index.py +0 -2
- rucio/db/sqla/migrate_repo/versions/45378a1e76a8_create_collection_replica_table.py +2 -4
- rucio/db/sqla/migrate_repo/versions/469d262be19_removing_created_at_index.py +0 -2
- rucio/db/sqla/migrate_repo/versions/4783c1f49cb4_create_distance_table.py +1 -3
- rucio/db/sqla/migrate_repo/versions/49a21b4d4357_create_index_on_table_tokens.py +1 -4
- rucio/db/sqla/migrate_repo/versions/4a2cbedda8b9_add_source_replica_expression_column_to_.py +0 -3
- rucio/db/sqla/migrate_repo/versions/4a7182d9578b_added_bytes_length_accessed_at_columns.py +0 -3
- rucio/db/sqla/migrate_repo/versions/4bab9edd01fc_create_index_on_requests_rule_id.py +0 -2
- rucio/db/sqla/migrate_repo/versions/4c3a4acfe006_new_attr_account_table.py +1 -3
- rucio/db/sqla/migrate_repo/versions/4cf0a2e127d4_adding_transient_metadata.py +0 -3
- rucio/db/sqla/migrate_repo/versions/4df2c5ddabc0_remove_temporary_dids.py +55 -0
- rucio/db/sqla/migrate_repo/versions/50280c53117c_add_qos_class_to_rse.py +0 -2
- rucio/db/sqla/migrate_repo/versions/52153819589c_add_rse_id_to_replicas_table.py +0 -2
- rucio/db/sqla/migrate_repo/versions/52fd9f4916fa_added_activity_to_rules.py +0 -3
- rucio/db/sqla/migrate_repo/versions/53b479c3cb0f_fix_did_meta_table_missing_updated_at_.py +0 -3
- rucio/db/sqla/migrate_repo/versions/5673b4b6e843_add_wfms_metadata_to_rule_tables.py +0 -3
- rucio/db/sqla/migrate_repo/versions/575767d9f89_added_source_history_table.py +1 -2
- rucio/db/sqla/migrate_repo/versions/58bff7008037_add_started_at_to_requests.py +0 -3
- rucio/db/sqla/migrate_repo/versions/58c8b78301ab_rename_callback_to_message.py +1 -3
- rucio/db/sqla/migrate_repo/versions/5f139f77382a_added_child_rule_id_column.py +1 -3
- rucio/db/sqla/migrate_repo/versions/688ef1840840_adding_did_meta_table.py +1 -2
- rucio/db/sqla/migrate_repo/versions/6e572a9bfbf3_add_new_split_container_column_to_rules.py +0 -3
- rucio/db/sqla/migrate_repo/versions/70587619328_add_comment_column_for_subscriptions.py +0 -3
- rucio/db/sqla/migrate_repo/versions/739064d31565_remove_history_table_pks.py +1 -2
- rucio/db/sqla/migrate_repo/versions/7541902bf173_add_didsfollowed_and_followevents_table.py +2 -4
- rucio/db/sqla/migrate_repo/versions/7ec22226cdbf_new_replica_state_for_temporary_.py +0 -1
- rucio/db/sqla/migrate_repo/versions/810a41685bc1_added_columns_rse_transfer_limits.py +1 -4
- rucio/db/sqla/migrate_repo/versions/83f991c63a93_correct_rse_expression_length.py +0 -2
- rucio/db/sqla/migrate_repo/versions/8523998e2e76_increase_size_of_extended_attributes_.py +0 -3
- rucio/db/sqla/migrate_repo/versions/8ea9122275b1_adding_missing_function_based_indices.py +1 -2
- rucio/db/sqla/migrate_repo/versions/90f47792bb76_add_clob_payload_to_messages.py +0 -3
- rucio/db/sqla/migrate_repo/versions/914b8f02df38_new_table_for_lifetime_model_exceptions.py +1 -3
- rucio/db/sqla/migrate_repo/versions/94a5961ddbf2_add_estimator_columns.py +0 -3
- rucio/db/sqla/migrate_repo/versions/9a1b149a2044_add_saml_identity_type.py +0 -1
- rucio/db/sqla/migrate_repo/versions/9a45bc4ea66d_add_vp_table.py +1 -2
- rucio/db/sqla/migrate_repo/versions/9eb936a81eb1_true_is_true.py +0 -2
- rucio/db/sqla/migrate_repo/versions/a08fa8de1545_transfer_stats_table.py +55 -0
- rucio/db/sqla/migrate_repo/versions/a118956323f8_added_vo_table_and_vo_col_to_rse.py +1 -3
- rucio/db/sqla/migrate_repo/versions/a193a275255c_add_status_column_in_messages.py +0 -2
- rucio/db/sqla/migrate_repo/versions/a5f6f6e928a7_1_7_0.py +1 -4
- rucio/db/sqla/migrate_repo/versions/a616581ee47_added_columns_to_table_requests.py +0 -1
- rucio/db/sqla/migrate_repo/versions/a6eb23955c28_state_idx_non_functional.py +0 -1
- rucio/db/sqla/migrate_repo/versions/a74275a1ad30_added_global_quota_table.py +1 -3
- rucio/db/sqla/migrate_repo/versions/a93e4e47bda_heartbeats.py +1 -4
- rucio/db/sqla/migrate_repo/versions/ae2a56fcc89_added_comment_column_to_rules.py +0 -1
- rucio/db/sqla/migrate_repo/versions/b0070f3695c8_add_deletedidmeta_table.py +57 -0
- rucio/db/sqla/migrate_repo/versions/b4293a99f344_added_column_identity_to_table_tokens.py +0 -3
- rucio/db/sqla/migrate_repo/versions/b5493606bbf5_fix_primary_key_for_subscription_history.py +41 -0
- rucio/db/sqla/migrate_repo/versions/b7d287de34fd_removal_of_replicastate_source.py +1 -2
- rucio/db/sqla/migrate_repo/versions/b818052fa670_add_index_to_quarantined_replicas.py +1 -3
- rucio/db/sqla/migrate_repo/versions/b8caac94d7f0_add_comments_column_for_subscriptions_.py +0 -3
- rucio/db/sqla/migrate_repo/versions/b96a1c7e1cc4_new_bad_pfns_table_and_bad_replicas_.py +1 -5
- rucio/db/sqla/migrate_repo/versions/bb695f45c04_extend_request_state.py +1 -3
- rucio/db/sqla/migrate_repo/versions/bc68e9946deb_add_staging_timestamps_to_request.py +0 -3
- rucio/db/sqla/migrate_repo/versions/bf3baa1c1474_correct_pk_and_idx_for_history_tables.py +1 -3
- rucio/db/sqla/migrate_repo/versions/c0937668555f_add_qos_policy_map_table.py +1 -2
- rucio/db/sqla/migrate_repo/versions/c129ccdb2d5_add_lumiblocknr_to_dids.py +0 -3
- rucio/db/sqla/migrate_repo/versions/ccdbcd48206e_add_did_type_column_index_on_did_meta_.py +1 -4
- rucio/db/sqla/migrate_repo/versions/cebad904c4dd_new_payload_column_for_heartbeats.py +1 -2
- rucio/db/sqla/migrate_repo/versions/d1189a09c6e0_oauth2_0_and_jwt_feature_support_adding_.py +1 -4
- rucio/db/sqla/migrate_repo/versions/d23453595260_extend_request_state_for_preparer.py +1 -3
- rucio/db/sqla/migrate_repo/versions/d6dceb1de2d_added_purge_column_to_rules.py +1 -4
- rucio/db/sqla/migrate_repo/versions/d6e2c3b2cf26_remove_third_party_copy_column_from_rse.py +0 -2
- rucio/db/sqla/migrate_repo/versions/d91002c5841_new_account_limits_table.py +1 -3
- rucio/db/sqla/migrate_repo/versions/e138c364ebd0_extending_columns_for_filter_and_.py +0 -3
- rucio/db/sqla/migrate_repo/versions/e59300c8b179_support_for_archive.py +1 -3
- rucio/db/sqla/migrate_repo/versions/f1b14a8c2ac1_postgres_use_check_constraints.py +0 -1
- rucio/db/sqla/migrate_repo/versions/f41ffe206f37_oracle_global_temporary_tables.py +1 -2
- rucio/db/sqla/migrate_repo/versions/f85a2962b021_adding_transfertool_column_to_requests_.py +1 -3
- rucio/db/sqla/migrate_repo/versions/fa7a7d78b602_increase_refresh_token_size.py +0 -2
- rucio/db/sqla/migrate_repo/versions/fb28a95fe288_add_replicas_rse_id_tombstone_idx.py +0 -1
- rucio/db/sqla/migrate_repo/versions/fe1a65b176c9_set_third_party_copy_read_and_write_.py +1 -2
- rucio/db/sqla/migrate_repo/versions/fe8ea2fa9788_added_third_party_copy_column_to_rse_.py +0 -3
- rucio/db/sqla/models.py +122 -216
- rucio/db/sqla/sautils.py +12 -5
- rucio/db/sqla/session.py +71 -43
- rucio/db/sqla/types.py +3 -4
- rucio/db/sqla/util.py +91 -69
- rucio/gateway/__init__.py +13 -0
- rucio/{api → gateway}/account.py +119 -46
- rucio/{api → gateway}/account_limit.py +12 -13
- rucio/{api → gateway}/authentication.py +106 -33
- rucio/{api → gateway}/config.py +12 -13
- rucio/{api → gateway}/credential.py +15 -4
- rucio/{api → gateway}/did.py +384 -140
- rucio/{api → gateway}/dirac.py +16 -6
- rucio/{api → gateway}/exporter.py +3 -4
- rucio/{api → gateway}/heartbeat.py +17 -5
- rucio/{api → gateway}/identity.py +63 -19
- rucio/{api → gateway}/importer.py +3 -4
- rucio/{api → gateway}/lifetime_exception.py +35 -10
- rucio/{api → gateway}/lock.py +34 -12
- rucio/{api/meta.py → gateway/meta_conventions.py} +18 -16
- rucio/{api → gateway}/permission.py +4 -5
- rucio/{api → gateway}/quarantined_replica.py +13 -4
- rucio/{api → gateway}/replica.py +12 -11
- rucio/{api → gateway}/request.py +129 -28
- rucio/{api → gateway}/rse.py +11 -12
- rucio/{api → gateway}/rule.py +117 -35
- rucio/{api → gateway}/scope.py +24 -14
- rucio/{api → gateway}/subscription.py +65 -43
- rucio/{api → gateway}/vo.py +17 -7
- rucio/rse/__init__.py +3 -4
- rucio/rse/protocols/__init__.py +0 -1
- rucio/rse/protocols/bittorrent.py +184 -0
- rucio/rse/protocols/cache.py +1 -2
- rucio/rse/protocols/dummy.py +1 -2
- rucio/rse/protocols/gfal.py +12 -10
- rucio/rse/protocols/globus.py +7 -7
- rucio/rse/protocols/gsiftp.py +2 -3
- rucio/rse/protocols/http_cache.py +1 -2
- rucio/rse/protocols/mock.py +1 -2
- rucio/rse/protocols/ngarc.py +1 -2
- rucio/rse/protocols/posix.py +12 -13
- rucio/rse/protocols/protocol.py +116 -52
- rucio/rse/protocols/rclone.py +6 -7
- rucio/rse/protocols/rfio.py +4 -5
- rucio/rse/protocols/srm.py +9 -10
- rucio/rse/protocols/ssh.py +8 -9
- rucio/rse/protocols/storm.py +2 -3
- rucio/rse/protocols/webdav.py +17 -14
- rucio/rse/protocols/xrootd.py +23 -17
- rucio/rse/rsemanager.py +19 -7
- rucio/tests/__init__.py +0 -1
- rucio/tests/common.py +43 -17
- rucio/tests/common_server.py +3 -3
- rucio/transfertool/__init__.py +0 -1
- rucio/transfertool/bittorrent.py +199 -0
- rucio/transfertool/bittorrent_driver.py +52 -0
- rucio/transfertool/bittorrent_driver_qbittorrent.py +133 -0
- rucio/transfertool/fts3.py +250 -138
- rucio/transfertool/fts3_plugins.py +152 -0
- rucio/transfertool/globus.py +9 -8
- rucio/transfertool/globus_library.py +1 -2
- rucio/transfertool/mock.py +21 -12
- rucio/transfertool/transfertool.py +33 -24
- rucio/vcsversion.py +4 -4
- rucio/version.py +5 -13
- rucio/web/__init__.py +0 -1
- rucio/web/rest/__init__.py +0 -1
- rucio/web/rest/flaskapi/__init__.py +0 -1
- rucio/web/rest/flaskapi/authenticated_bp.py +0 -1
- rucio/web/rest/flaskapi/v1/__init__.py +0 -1
- rucio/web/rest/flaskapi/v1/accountlimits.py +15 -13
- rucio/web/rest/flaskapi/v1/accounts.py +49 -48
- rucio/web/rest/flaskapi/v1/archives.py +12 -10
- rucio/web/rest/flaskapi/v1/auth.py +146 -144
- rucio/web/rest/flaskapi/v1/common.py +82 -41
- rucio/web/rest/flaskapi/v1/config.py +5 -6
- rucio/web/rest/flaskapi/v1/credentials.py +7 -8
- rucio/web/rest/flaskapi/v1/dids.py +158 -28
- rucio/web/rest/flaskapi/v1/dirac.py +8 -8
- rucio/web/rest/flaskapi/v1/export.py +3 -5
- rucio/web/rest/flaskapi/v1/heartbeats.py +3 -5
- rucio/web/rest/flaskapi/v1/identities.py +3 -5
- rucio/web/rest/flaskapi/v1/import.py +3 -4
- rucio/web/rest/flaskapi/v1/lifetime_exceptions.py +6 -9
- rucio/web/rest/flaskapi/v1/locks.py +2 -4
- rucio/web/rest/flaskapi/v1/main.py +10 -2
- rucio/web/rest/flaskapi/v1/{meta.py → meta_conventions.py} +26 -11
- rucio/web/rest/flaskapi/v1/metrics.py +1 -2
- rucio/web/rest/flaskapi/v1/nongrid_traces.py +4 -4
- rucio/web/rest/flaskapi/v1/ping.py +6 -7
- rucio/web/rest/flaskapi/v1/redirect.py +8 -9
- rucio/web/rest/flaskapi/v1/replicas.py +43 -19
- rucio/web/rest/flaskapi/v1/requests.py +178 -21
- rucio/web/rest/flaskapi/v1/rses.py +61 -26
- rucio/web/rest/flaskapi/v1/rules.py +48 -18
- rucio/web/rest/flaskapi/v1/scopes.py +3 -5
- rucio/web/rest/flaskapi/v1/subscriptions.py +22 -18
- rucio/web/rest/flaskapi/v1/traces.py +4 -4
- rucio/web/rest/flaskapi/v1/types.py +20 -0
- rucio/web/rest/flaskapi/v1/vos.py +3 -5
- rucio/web/rest/main.py +0 -1
- rucio/web/rest/metrics.py +0 -1
- rucio/web/rest/ping.py +27 -0
- {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/ldap.cfg.template +1 -1
- rucio-35.8.0.data/data/rucio/requirements.server.txt +268 -0
- {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/tools/bootstrap.py +3 -3
- {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/tools/merge_rucio_configs.py +2 -5
- {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/tools/reset_database.py +3 -3
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio +87 -85
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-abacus-account +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-abacus-collection-replica +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-abacus-rse +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-admin +45 -32
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-atropos +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-auditor +13 -7
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-automatix +1 -2
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-bb8 +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-c3po +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-cache-client +2 -3
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-cache-consumer +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-finisher +1 -2
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-poller +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-preparer +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-receiver +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-stager +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-submitter +2 -3
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-conveyor-throttler +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-dark-reaper +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-dumper +11 -10
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-follower +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-hermes +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-judge-cleaner +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-judge-evaluator +2 -3
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-judge-injector +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-judge-repairer +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-kronos +1 -3
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-minos +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-minos-temporary-expiration +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-necromancer +1 -2
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-oauth-manager +2 -3
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-reaper +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-replica-recoverer +6 -7
- rucio-35.8.0.data/scripts/rucio-rse-decommissioner +66 -0
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-storage-consistency-actions +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-transmogrifier +0 -1
- {rucio-32.8.6.data → rucio-35.8.0.data}/scripts/rucio-undertaker +1 -2
- rucio-35.8.0.dist-info/METADATA +72 -0
- rucio-35.8.0.dist-info/RECORD +493 -0
- {rucio-32.8.6.dist-info → rucio-35.8.0.dist-info}/WHEEL +1 -1
- {rucio-32.8.6.dist-info → rucio-35.8.0.dist-info}/licenses/AUTHORS.rst +3 -0
- rucio/api/temporary_did.py +0 -49
- rucio/common/schema/cms.py +0 -478
- rucio/common/schema/lsst.py +0 -423
- rucio/core/permission/cms.py +0 -1166
- rucio/core/temporary_did.py +0 -188
- rucio/daemons/reaper/light_reaper.py +0 -255
- rucio/web/rest/flaskapi/v1/tmp_dids.py +0 -115
- rucio-32.8.6.data/data/rucio/requirements.txt +0 -55
- rucio-32.8.6.data/scripts/rucio-light-reaper +0 -53
- rucio-32.8.6.dist-info/METADATA +0 -83
- rucio-32.8.6.dist-info/RECORD +0 -481
- {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/alembic.ini.template +0 -0
- {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/alembic_offline.ini.template +0 -0
- {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/globus-config.yml.template +0 -0
- {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/mail_templates/rule_approval_request.tmpl +0 -0
- {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +0 -0
- {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/mail_templates/rule_approved_user.tmpl +0 -0
- {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +0 -0
- {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/mail_templates/rule_denied_user.tmpl +0 -0
- {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +0 -0
- {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/rse-accounts.cfg.template +0 -0
- {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/rucio.cfg.atlas.client.template +0 -0
- {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/rucio.cfg.template +0 -0
- {rucio-32.8.6.data → rucio-35.8.0.data}/data/rucio/etc/rucio_multi_vo.cfg.template +0 -0
- {rucio-32.8.6.dist-info → rucio-35.8.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
22
|
-
from collections
|
|
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_,
|
|
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,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
from rucio.common.
|
|
32
|
-
from rucio.common.
|
|
33
|
-
from rucio.common.
|
|
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
|
|
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
|
|
39
|
-
from rucio.db.sqla.constants import
|
|
40
|
-
from rucio.db.sqla.session import read_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__(
|
|
60
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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=
|
|
555
|
-
source = RequestSource(
|
|
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(
|
|
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
|
-
|
|
578
|
-
|
|
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(
|
|
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
|
-
|
|
600
|
-
|
|
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:
|
|
632
|
-
activity:
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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.
|
|
845
|
+
query = query.where(models.Request.updated_at < older_than)
|
|
727
846
|
|
|
728
847
|
if rse_id:
|
|
729
|
-
query = query.
|
|
848
|
+
query = query.where(models.Request.dest_rse_id == rse_id)
|
|
730
849
|
|
|
731
850
|
if share:
|
|
732
|
-
query = query.
|
|
851
|
+
query = query.where(models.Request.activity == share)
|
|
733
852
|
elif activity:
|
|
734
|
-
query = query.
|
|
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
|
-
|
|
788
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1081
|
+
'/*+ INDEX(REQUESTS REQUESTS_RULEID_IDX) */',
|
|
1082
|
+
dialect='oracle'
|
|
956
1083
|
).where(
|
|
957
|
-
models.Request.rule_id == rule_id,
|
|
958
|
-
|
|
959
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1018
|
-
|
|
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(
|
|
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
|
-
|
|
1060
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
1115
|
-
err_msg
|
|
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(
|
|
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
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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(
|
|
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
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
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-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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,
|
|
2096
|
+
models.Request,
|
|
2097
|
+
'INDEX(REQUESTS REQUESTS_TYP_STA_UPD_IDX)',
|
|
2098
|
+
'oracle'
|
|
1330
2099
|
).where(
|
|
1331
|
-
models.Request.state.in_(state),
|
|
1332
|
-
|
|
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
|
-
)
|
|
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
|
-
|
|
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
|
|
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
|
-
)
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1856
|
-
|
|
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
|
|
1926
|
-
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
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
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
|
-
|
|
1964
|
-
|
|
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(
|
|
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.
|
|
2801
|
+
query = query.where(models.ReplicaLock.rule_id == filter_['rule_id'])
|
|
2015
2802
|
if 'request_id' in filter_:
|
|
2016
|
-
query = query.
|
|
2803
|
+
query = query.where(models.Request.id == filter_['request_id'])
|
|
2017
2804
|
if 'older_than' in filter_:
|
|
2018
|
-
query = query.
|
|
2805
|
+
query = query.where(models.Request.created_at < filter_['older_than'])
|
|
2019
2806
|
if 'activities' in filter_:
|
|
2020
|
-
if
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
2208
|
-
|
|
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(
|
|
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
|
-
).
|
|
2232
|
-
models.RequestHistory.state.in_(states),
|
|
2233
|
-
|
|
2234
|
-
|
|
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)
|